All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.netflix.astyanax.thrift.ThriftKeyspaceImpl Maven / Gradle / Ivy

The newest version!
/*******************************************************************************
 * Copyright 2011 Netflix
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/
package com.netflix.astyanax.thrift;

import java.nio.ByteBuffer;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

import org.apache.cassandra.thrift.Cassandra;
import org.apache.cassandra.thrift.Cassandra.Client;
import org.apache.cassandra.thrift.CfDef;
import org.apache.cassandra.thrift.CounterColumn;
import org.apache.cassandra.thrift.KsDef;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Function;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.netflix.astyanax.AstyanaxConfiguration;
import com.netflix.astyanax.CassandraOperationType;
import com.netflix.astyanax.ColumnMutation;
import com.netflix.astyanax.Execution;
import com.netflix.astyanax.Keyspace;
import com.netflix.astyanax.KeyspaceTracerFactory;
import com.netflix.astyanax.MutationBatch;
import com.netflix.astyanax.SerializerPackage;
import com.netflix.astyanax.WriteAheadEntry;
import com.netflix.astyanax.WriteAheadLog;
import com.netflix.astyanax.connectionpool.ConnectionContext;
import com.netflix.astyanax.connectionpool.ConnectionPool;
import com.netflix.astyanax.connectionpool.Host;
import com.netflix.astyanax.connectionpool.Operation;
import com.netflix.astyanax.connectionpool.OperationResult;
import com.netflix.astyanax.connectionpool.TokenRange;
import com.netflix.astyanax.connectionpool.exceptions.BadRequestException;
import com.netflix.astyanax.connectionpool.exceptions.ConnectionException;
import com.netflix.astyanax.connectionpool.exceptions.IsDeadConnectionException;
import com.netflix.astyanax.connectionpool.exceptions.NotFoundException;
import com.netflix.astyanax.connectionpool.exceptions.OperationException;
import com.netflix.astyanax.connectionpool.exceptions.SchemaDisagreementException;
import com.netflix.astyanax.connectionpool.impl.OperationResultImpl;
import com.netflix.astyanax.connectionpool.impl.TokenRangeImpl;
import com.netflix.astyanax.cql.CqlStatement;
import com.netflix.astyanax.ddl.ColumnFamilyDefinition;
import com.netflix.astyanax.ddl.KeyspaceDefinition;
import com.netflix.astyanax.ddl.SchemaChangeResult;
import com.netflix.astyanax.ddl.impl.SchemaChangeResponseImpl;
import com.netflix.astyanax.model.CfSplit;
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.retry.RunOnce;
import com.netflix.astyanax.serializers.SerializerPackageImpl;
import com.netflix.astyanax.serializers.UnknownComparatorException;
import com.netflix.astyanax.thrift.ddl.ThriftColumnFamilyDefinitionImpl;
import com.netflix.astyanax.thrift.ddl.ThriftKeyspaceDefinitionImpl;
import com.netflix.astyanax.thrift.model.ThriftCfSplitImpl;

/**
 * 
 * @ThreadSafe
 * Note that the same instances of this class can be used by multiple threads. 
 * Especially for reads and writes, the prepareQuery() and prepareMutationBatch() calls are thread safe
 * but adding multiple mutations to the same mutation batch is NOT thread safe. 
 * 
 * Hence once a mutation batch is created by a thread, then that batch should be used by that thread only.
 * 
 * @author elandau
 *
 */
public final class ThriftKeyspaceImpl implements Keyspace {
    private final static Logger LOG = LoggerFactory.getLogger(ThriftKeyspaceImpl.class);
    
    final ConnectionPool connectionPool;
    final AstyanaxConfiguration config;
    final String                ksName;
    final ListeningExecutorService executor;
    final KeyspaceTracerFactory tracerFactory;
    final Cache cache;
    final ThriftCqlFactory      cqlStatementFactory;
    private Host                  ddlHost = null;
    private volatile Partitioner  partitioner;
    
    public ThriftKeyspaceImpl(
            String ksName, 
            ConnectionPool pool, 
            AstyanaxConfiguration config,
            final KeyspaceTracerFactory tracerFactory) {
        this.connectionPool = pool;
        this.config         = config;
        this.ksName         = ksName;
        this.executor       = MoreExecutors.listeningDecorator(config.getAsyncExecutor());
        this.tracerFactory  = tracerFactory;
        this.cache          = CacheBuilder.newBuilder().expireAfterWrite(10, TimeUnit.MINUTES).build();
        this.cqlStatementFactory = ThriftCqlFactoryResolver.createFactory(config);
    }

    @Override
    public String getKeyspaceName() {
        return this.ksName;
    }

    @Override
    public MutationBatch prepareMutationBatch() {
        return new AbstractThriftMutationBatchImpl(config.getClock(), config.getDefaultWriteConsistencyLevel(), config.getRetryPolicy().duplicate()) {
            @Override
            public OperationResult execute() throws ConnectionException {
                WriteAheadLog wal = getWriteAheadLog();
                WriteAheadEntry walEntry = null;
                if (wal != null) {
                    walEntry = wal.createEntry();
                    walEntry.writeMutation(this);
                }
                try {
                    OperationResult result = executeOperation(
                            new AbstractKeyspaceOperationImpl(
                                    tracerFactory.newTracer(useAtomicBatch() ? CassandraOperationType.ATOMIC_BATCH_MUTATE : CassandraOperationType.BATCH_MUTATE), 
                                                            getPinnedHost(),
                                                            getKeyspaceName()) {
                                @Override
                                public Void internalExecute(Client client, ConnectionContext context) throws Exception {
                                    // Mutation can be atomic or non-atomic. 
                                    // see http://www.datastax.com/dev/blog/atomic-batches-in-cassandra-1-2 for details on atomic batches
                                    if (useAtomicBatch()) {
                                        client.atomic_batch_mutate(getMutationMap(),
                                                ThriftConverter.ToThriftConsistencyLevel(getConsistencyLevel()));
                                    } else {
                                        client.batch_mutate(getMutationMap(),
                                                ThriftConverter.ToThriftConsistencyLevel(getConsistencyLevel()));
                                    }
                                    discardMutations();
                                    return null;
                                }

                                @Override
                                public ByteBuffer getRowKey() {
                                    if (getMutationMap().size() == 1)
                                        return getMutationMap().keySet().iterator().next();
                                    else
                                        return null;
                                }
                            }, getRetryPolicy());

                    if (walEntry != null) {
                        wal.removeEntry(walEntry);
                    }
                    return result;
                }
                catch (ConnectionException exception) {
                    throw exception;
                }
                catch (Exception exception) {
                    throw ThriftConverter.ToConnectionPoolException(exception);
                }
            }

            @Override
            public ListenableFuture> executeAsync() throws ConnectionException {
                return executor.submit(new Callable>() {
                    @Override
                    public OperationResult call() throws Exception {
                        return execute();
                    }
                });
            }

			@Override
			public MutationBatch withCaching(boolean condition) {
				// This is a no-op when using Thrift
				return this;
			}
        };
    }

    @Override
    public List describeRing() throws ConnectionException {
        return describeRing(null, null);
    }

    @Override
    public List describeRing(final String dc) throws ConnectionException {
        return describeRing(dc, null);
    }

    @Override
    public List describeRing(final String dc, final String rack) throws ConnectionException {
        return executeOperation(
                new AbstractKeyspaceOperationImpl>(tracerFactory
                        .newTracer(CassandraOperationType.DESCRIBE_RING), getKeyspaceName()) {
                    @Override
                    public List internalExecute(Cassandra.Client client, ConnectionContext context) throws Exception {
                        List trs = client.describe_ring(getKeyspaceName());
                        List range = Lists.newArrayList();

                        for (org.apache.cassandra.thrift.TokenRange tr : trs) {
                            List endpoints = Lists.newArrayList();
                            for (org.apache.cassandra.thrift.EndpointDetails ed : tr.getEndpoint_details()) {
                                if (dc != null && !ed.getDatacenter().equals(dc)) {
                                    continue;
                                }
                                else if (rack != null && !ed.getRack().equals(rack)) {
                                    continue;
                                }
                                else {
                                    endpoints.add(ed.getHost());
                                }
                            }

                            if (!endpoints.isEmpty()) {
                                range.add(new TokenRangeImpl(tr.getStart_token(), tr.getEnd_token(), endpoints));
                            }
                        }
                        return range;
                    }
                }, getConfig().getRetryPolicy().duplicate()).getResult();
    }

    @SuppressWarnings("unchecked")
    @Override
    public List describeRing(boolean cached) throws ConnectionException {
        if (cached) {
          try {
            return (List) this.cache.get(CassandraOperationType.DESCRIBE_RING.name(),
                  new Callable() {
                      @Override
                      public Object call() throws Exception {
                          return describeRing();
                      }
                  });
            }
            catch (ExecutionException e) {
                throw ThriftConverter.ToConnectionPoolException(e);
            }
        }
        else {
            return describeRing();
        }
    }

    @Override
    public KeyspaceDefinition describeKeyspace() throws ConnectionException {
    	return internalDescribeKeyspace().getResult();
    }
    
    public OperationResult internalDescribeKeyspace() throws ConnectionException {
        return executeOperation(
                new AbstractKeyspaceOperationImpl(
                        tracerFactory.newTracer(CassandraOperationType.DESCRIBE_KEYSPACE), getKeyspaceName()) {
                    @Override
                    public KeyspaceDefinition internalExecute(Cassandra.Client client, ConnectionContext context) throws Exception {
                        return new ThriftKeyspaceDefinitionImpl(client.describe_keyspace(getKeyspaceName()));
                    }
                }, getConfig().getRetryPolicy().duplicate());
    }

    @Override
    public Map> describeSchemaVersions() throws ConnectionException {
        return connectionPool.executeWithFailover(
                new AbstractOperationImpl>>(
                        tracerFactory.newTracer(CassandraOperationType.DESCRIBE_SCHEMA_VERSION)) {
                    @Override
                    public Map> internalExecute(Client client, ConnectionContext context) throws Exception {
                        return client.describe_schema_versions();
                    }
                }, config.getRetryPolicy().duplicate()).getResult();
    }

    public  ColumnFamilyQuery prepareQuery(ColumnFamily cf) {
        return new ThriftColumnFamilyQueryImpl(
                executor,
                tracerFactory,
                this,
                connectionPool,
                cf,
                config.getDefaultReadConsistencyLevel(),
                config.getRetryPolicy().duplicate());
    }

    @Override
    public  ColumnMutation prepareColumnMutation(final ColumnFamily columnFamily, final K rowKey, C column) {
        return new AbstractThriftColumnMutationImpl(
                columnFamily.getKeySerializer().toByteBuffer(rowKey),
                columnFamily.getColumnSerializer().toByteBuffer(column),
                config) {

            @Override
            public Execution incrementCounterColumn(final long amount) {
                return new Execution() {
                    @Override
                    public OperationResult execute() throws ConnectionException {
                        return executeOperation(
                                new AbstractKeyspaceOperationImpl(
                                        tracerFactory.newTracer(CassandraOperationType.COUNTER_MUTATE),
                                        getKeyspaceName()) {
                                    @Override
                                    public Void internalExecute(Client client, ConnectionContext context) throws Exception {
                                        client.add(key, ThriftConverter.getColumnParent(columnFamily, null),
                                                new CounterColumn().setValue(amount).setName(column),
                                                ThriftConverter.ToThriftConsistencyLevel(writeConsistencyLevel));
                                        return null;
                                    }

                                    @Override
                                    public ByteBuffer getRowKey() {
                                        return columnFamily.getKeySerializer()
                                                .toByteBuffer(rowKey);
                                    }
                                }, retry);
                    }

                    @Override
                    public ListenableFuture> executeAsync() throws ConnectionException {
                        return executor.submit(new Callable>() {
                            @Override
                            public OperationResult call() throws Exception {
                                return execute();
                            }
                        });
                    }
                };
            }

            @Override
            public Execution deleteColumn() {
                return new Execution() {
                    @Override
                    public OperationResult execute() throws ConnectionException {
                        return executeOperation(
                                new AbstractKeyspaceOperationImpl(
                                        tracerFactory.newTracer(CassandraOperationType.COLUMN_DELETE),
                                        getKeyspaceName()) {
                                    @Override
                                    public Void internalExecute(Client client, ConnectionContext context) throws Exception {
                                        client.remove(key, new org.apache.cassandra.thrift.ColumnPath()
                                                .setColumn_family(columnFamily.getName()).setColumn(column), config
                                                .getClock().getCurrentTime(), ThriftConverter
                                                .ToThriftConsistencyLevel(writeConsistencyLevel));
                                        return null;
                                    }

                                    @Override
                                    public ByteBuffer getRowKey() {
                                        return columnFamily.getKeySerializer().toByteBuffer(rowKey);
                                    }
                                }, retry);
                    }

                    @Override
                    public ListenableFuture> executeAsync() throws ConnectionException {
                        return executor.submit(new Callable>() {
                            @Override
                            public OperationResult call() throws Exception {
                                return execute();
                            }
                        });
                    }
                };
            }

            @Override
            public Execution insertValue(final ByteBuffer value, final Integer ttl) {
                return new Execution() {
                    @Override
                    public OperationResult execute() throws ConnectionException {
                        return executeOperation(
                                new AbstractKeyspaceOperationImpl(
                                        tracerFactory.newTracer(CassandraOperationType.COLUMN_INSERT),
                                        getKeyspaceName()) {
                                    @Override
                                    public Void internalExecute(Client client, ConnectionContext context) throws Exception {
                                        org.apache.cassandra.thrift.Column c = new org.apache.cassandra.thrift.Column();
                                        c.setName(column).setValue(value).setTimestamp(clock.getCurrentTime());
                                        if (ttl != null) {
                                            c.setTtl(ttl);
                                        }

                                        client.insert(key, ThriftConverter.getColumnParent(columnFamily, null), c,
                                                ThriftConverter.ToThriftConsistencyLevel(writeConsistencyLevel));
                                        return null;
                                    }

                                    @Override
                                    public ByteBuffer getRowKey() {
                                        return columnFamily.getKeySerializer().toByteBuffer(rowKey);
                                    }
                                }, retry);
                    }

                    @Override
                    public ListenableFuture> executeAsync() throws ConnectionException {
                        return executor.submit(new Callable>() {
                            @Override
                            public OperationResult call() throws Exception {
                                return execute();
                            }
                        });
                    }
                };
            }

            @Override
            public Execution deleteCounterColumn() {
                return new Execution() {
                    @Override
                    public OperationResult execute() throws ConnectionException {
                        return executeOperation(
                                new AbstractKeyspaceOperationImpl(
                                        tracerFactory.newTracer(CassandraOperationType.COLUMN_DELETE),
                                        getKeyspaceName()) {
                                    @Override
                                    public Void internalExecute(Client client, ConnectionContext context) throws Exception {
                                        client.remove_counter(key, new org.apache.cassandra.thrift.ColumnPath()
                                                .setColumn_family(columnFamily.getName()).setColumn(column),
                                                ThriftConverter.ToThriftConsistencyLevel(writeConsistencyLevel));
                                        return null;
                                    }

                                    @Override
                                    public ByteBuffer getRowKey() {
                                        return columnFamily.getKeySerializer().toByteBuffer(rowKey);
                                    }
                                }, retry);
                    }

                    @Override
                    public ListenableFuture> executeAsync() throws ConnectionException {
                        return executor.submit(new Callable>() {
                            @Override
                            public OperationResult call() throws Exception {
                                return execute();
                            }
                        });
                    }
                };
            }
        };
    }

    @Override
    public AstyanaxConfiguration getConfig() {
        return this.config;
    }

    @Override
    public SerializerPackage getSerializerPackage(String cfName, boolean ignoreErrors) throws ConnectionException,
            UnknownComparatorException {
        return new SerializerPackageImpl(describeKeyspace().getColumnFamily(cfName), ignoreErrors);
    }

    @Override
    public List describeSplits(final String cfName, final String startToken, final String endToken,
                                       final int keysPerSplit) throws ConnectionException {
        return executeOperation(
                new AbstractKeyspaceOperationImpl>(tracerFactory
                        .newTracer(CassandraOperationType.DESCRIBE_SPLITS), getKeyspaceName()) {
                    @Override
                    public List internalExecute(Client client, ConnectionContext state) throws Exception {
                        List tokens = client.describe_splits(cfName, startToken, endToken, keysPerSplit);
                        return Lists.transform(tokens, new Function() {
                            @Override
                            public String apply(String token) {
                                return sanitizeToken(token);

                            }
                        });
                    }
                }, getConfig().getRetryPolicy().duplicate()).getResult();
    }

    @Override
    public List describeSplitsEx(String cfName, String startToken, String endToken, int keysPerSplit)
            throws ConnectionException {
        return describeSplitsEx(cfName, startToken, endToken, keysPerSplit, null);
    }

    @Override
    public List describeSplitsEx(final String cfName, final String startToken, final String endToken,
            final int keysPerSplit, final ByteBuffer startKey) throws ConnectionException {
        return executeOperation(
                new AbstractKeyspaceOperationImpl>(tracerFactory
                        .newTracer(CassandraOperationType.DESCRIBE_SPLITS), getKeyspaceName()) {
                    @Override
                    public List internalExecute(Client client, ConnectionContext state) throws Exception {
                        List splits =
                                client.describe_splits_ex(cfName, startToken, endToken, keysPerSplit);
                        return Lists.transform(splits, new Function() {
                            @Override
                            public CfSplit apply(org.apache.cassandra.thrift.CfSplit split) {
                                return new ThriftCfSplitImpl(
                                        sanitizeToken(split.getStart_token()),
                                        sanitizeToken(split.getEnd_token()),
                                        split.getRow_count());
                            }
                        });
                    }

                    @Override
                    public ByteBuffer getRowKey() {
                        return startKey;
                    }
                }, getConfig().getRetryPolicy().duplicate()).getResult();
    }

    private String sanitizeToken(String token) {
        // In Cassandra 1.1.8+ describe_splits changed to return "Token(bytes[])" instead
        // of just "" when using the ByteOrderedPartitioner.  Preserve the old, more
        // sensible behavior.  See https://issues.apache.org/jira/browse/CASSANDRA-4803
        String prefix = "Token(bytes[", suffix = "])";
        if (token.startsWith(prefix) && token.endsWith(suffix)) {
            token = token.substring(prefix.length(), token.length() - suffix.length());
        }
        return token;
    }

    @Override
    public OperationResult testOperation(final Operation operation) throws ConnectionException {
        return testOperation(operation, config.getRetryPolicy().duplicate());
    }

    @Override
    public OperationResult testOperation(final Operation operation, RetryPolicy retry)
            throws ConnectionException {
        return executeOperation(
                new AbstractKeyspaceOperationImpl(tracerFactory.newTracer(CassandraOperationType.TEST),
                        operation.getPinnedHost(), getKeyspaceName()) {
                    @Override
                    public Void internalExecute(Client client, ConnectionContext context) throws Exception {
                        operation.execute(null, context);
                        return null;
                    }
                }, retry);
    }

    @Override
    public ConnectionPool getConnectionPool() {
        return connectionPool;
    }

    @Override
    public  OperationResult truncateColumnFamily(final ColumnFamily columnFamily)
            throws OperationException, ConnectionException {
        return truncateColumnFamily(columnFamily.getName());
    }

    @Override
    public OperationResult truncateColumnFamily(final String columnFamily) throws ConnectionException {
        return executeOperation(
                new AbstractKeyspaceOperationImpl(tracerFactory.newTracer(CassandraOperationType.TRUNCATE),
                        getKeyspaceName()) {
                    @Override
                    public Void internalExecute(Cassandra.Client client, ConnectionContext context) throws Exception {
                        client.truncate(columnFamily);
                        return null;
                    }
                }, config.getRetryPolicy().duplicate());
    }

    private  OperationResult executeOperation(Operation operation, RetryPolicy retry)
            throws OperationException, ConnectionException {
        return connectionPool.executeWithFailover(operation, retry);
    }

        
    /**
     * Attempt to execute the DDL operation on the same host
     * @param operation
     * @param retry
     * @return
     * @throws OperationException
     * @throws ConnectionException
     */
    private synchronized  OperationResult executeDdlOperation(AbstractOperationImpl operation, RetryPolicy retry)
             throws OperationException, ConnectionException {
         ConnectionException lastException = null;
         for (int i = 0; i < 2; i++) {
             operation.setPinnedHost(ddlHost);
             try {
                 OperationResult result = connectionPool.executeWithFailover(operation, retry);
                 ddlHost = result.getHost();
                 return result;
             }
             catch (ConnectionException e) {
                lastException = e;
                if (e instanceof IsDeadConnectionException) {
                     ddlHost = null;
                }
             }
         }
         throw lastException;
    }

    @Override
    public String describePartitioner() throws ConnectionException {
        return executeOperation(
                        new AbstractOperationImpl(
                                tracerFactory.newTracer(CassandraOperationType.DESCRIBE_PARTITIONER)) {
                            @Override
                            public String internalExecute(Client client, ConnectionContext context) throws Exception {
                                return client.describe_partitioner();
                            }
                        }, config.getRetryPolicy().duplicate()).getResult();
    }

    @Override
    public OperationResult createColumnFamily(final Map options) throws ConnectionException {
        final CfDef cfDef = toThriftColumnFamilyDefinition(options, null).getThriftColumnFamilyDefinition();
        return internalCreateColumnFamily(cfDef);
    }

    @Override
    public OperationResult createKeyspaceIfNotExists(final Map options) throws ConnectionException {

    	return createKeyspaceIfNotExists(new Callable>() {
			@Override
			public OperationResult call() throws Exception {
				return createKeyspace(options);
			}
    	});
    }
    
    @Override
    public OperationResult createKeyspace(
            final Map options,
            final Map> cfs) throws ConnectionException {
        
        ThriftKeyspaceDefinitionImpl ksDef = toThriftKeyspaceDefinition(options);
        for (Entry> cf : cfs.entrySet()) {
            ksDef.addColumnFamily(toThriftColumnFamilyDefinition(cf.getValue(), cf.getKey()));
        }
        
        ksDef.setName(getKeyspaceName());

        return internalCreateKeyspace(ksDef.getThriftKeyspaceDefinition());
    }    
    
    @Override
    public OperationResult createKeyspaceIfNotExists(
    		final Map options,
            final Map> cfs) throws ConnectionException {

    	return createKeyspaceIfNotExists(new Callable>() {
			@Override
			public OperationResult call() throws Exception {
				return createKeyspace(options, cfs);
			}
    	});
    }
    
    @Override
    public OperationResult createKeyspace(final Properties props) throws ConnectionException {
        if (props.containsKey("name") && !props.get("name").equals(getKeyspaceName())) { 
            throw new BadRequestException(
                    String.format("'name' attribute must match keyspace name. Expected '%s' but got '%s'", 
                                  getKeyspaceName(), props.get("name")));
        }
        
        final KsDef ksDef;
        try {
            ksDef = ThriftUtils.getThriftObjectFromProperties(KsDef.class, props);
        } catch (Exception e) {
            throw new BadRequestException("Unable to convert props to keyspace definition");
        }
        
        return internalCreateKeyspace(ksDef);
    }


    @Override
    public OperationResult createKeyspaceIfNotExists(final Properties props) throws ConnectionException {

    	return createKeyspaceIfNotExists(new Callable>() {
			@Override
			public OperationResult call() throws Exception {
				return createKeyspace(props);
			}
    	});
    }
    
    private OperationResult createKeyspaceIfNotExists(Callable> createKeyspace) throws ConnectionException {
        
    	boolean shouldCreateKeyspace = false;
    	
    	try { 
    		
    		OperationResult opResult = this.internalDescribeKeyspace();
        	
    		if (opResult != null && opResult.getResult() != null) {
        		return new OperationResultImpl(opResult.getHost(), 
                        new SchemaChangeResponseImpl().setSchemaId("no-op"), 
                        opResult.getLatency());

        	} else {
        		shouldCreateKeyspace = true;
        	}
        } catch (BadRequestException e) {
        	if (e.isKeyspaceDoestNotExist()) {
        		shouldCreateKeyspace = true;
        	} else {
        		throw e;
        	}
        }
    	
    	if (shouldCreateKeyspace) {
    		try {
				return createKeyspace.call();
			} catch (ConnectionException e) {
				throw e;
			} catch (Exception e) {
				throw new RuntimeException(e);
			}
    	} else {
    		throw new IllegalStateException();
    	}
    }


    @Override
    public  OperationResult createColumnFamily(final ColumnFamily columnFamily, final Map options) throws ConnectionException {
        final CfDef cfDef = toThriftColumnFamilyDefinition(options, columnFamily).getThriftColumnFamilyDefinition();
        return internalCreateColumnFamily(cfDef);
    }

    @Override
    public  OperationResult updateColumnFamily(final ColumnFamily columnFamily, final Map options) throws ConnectionException  {
        final CfDef cfDef = toThriftColumnFamilyDefinition(options, columnFamily).getThriftColumnFamilyDefinition();
        return internalUpdateColumnFamily(cfDef);
    }

    @Override
    public OperationResult dropColumnFamily(final String columnFamilyName) throws ConnectionException  {
        return executeDdlOperation(
                        new AbstractKeyspaceOperationImpl(
                                tracerFactory.newTracer(CassandraOperationType.DROP_COLUMN_FAMILY), getKeyspaceName()) {
                            @Override
                            public SchemaChangeResult internalExecute(Client client, ConnectionContext context) throws Exception {
                                precheckSchemaAgreement(client);
                                return new SchemaChangeResponseImpl()
                                    .setSchemaId(client.system_drop_column_family(columnFamilyName));
                            }
                        }, RunOnce.get());
    }

    @Override
    public  OperationResult dropColumnFamily(final ColumnFamily columnFamily) throws ConnectionException  {
        return dropColumnFamily(columnFamily.getName());
    }

    @Override
    public OperationResult createKeyspace(final Map options) throws ConnectionException  {
        final KsDef ksDef = toThriftKeyspaceDefinition(options).getThriftKeyspaceDefinition();
        return internalCreateKeyspace(ksDef);
    }


    @Override
    public OperationResult updateKeyspace(final Map options) throws ConnectionException  {
        final KsDef ksDef = toThriftKeyspaceDefinition(options).getThriftKeyspaceDefinition();
        return internalUpdateKeyspace(ksDef);
    }

    @Override
    public OperationResult dropKeyspace() throws ConnectionException  {
        return executeDdlOperation(
                        new AbstractKeyspaceOperationImpl(
                                tracerFactory.newTracer(CassandraOperationType.DROP_KEYSPACE), getKeyspaceName()) {
                            @Override
                            public SchemaChangeResult internalExecute(Client client, ConnectionContext context) throws Exception {
                                precheckSchemaAgreement(client);
                                return new SchemaChangeResponseImpl()
                                    .setSchemaId(client.system_drop_keyspace(getKeyspaceName()));
                            }
                        }, RunOnce.get());
    }

    @Override
    public CqlStatement prepareCqlStatement() {
        return this.cqlStatementFactory.createCqlStatement(this);
    }

    @Override
    public Partitioner getPartitioner() throws ConnectionException {
        if (partitioner == null) {
            synchronized(this) {
                if (partitioner == null) {
                    String partitionerName = this.describePartitioner();
                    try {
                        partitioner = config.getPartitioner(partitionerName);
                        LOG.info(String.format("Detected partitioner %s for keyspace %s", partitionerName, ksName));
                    } catch (Exception e) {
                        throw new NotFoundException("Unable to determine partitioner " + partitionerName, e);
                    }
                }
            }
        }
        return partitioner;
    }
    
    /**
     * Do a quick check to see if there is a schema disagreement.  This is done as an extra precaution
     * to reduce the chances of putting the cluster into a bad state.  This will not gurantee however, that 
     * by the time a schema change is made the cluster will be in the same state.
     * @param client
     * @throws Exception
     */
    private void precheckSchemaAgreement(Client client) throws Exception {
        Map> schemas = client.describe_schema_versions();
        if (schemas.size() > 1) {
            throw new SchemaDisagreementException("Can't change schema due to pending schema agreement");
        }
    }
    
    /**
     * Convert a Map of options to an internal thrift column family definition
     * @param options
     */
    private ThriftColumnFamilyDefinitionImpl toThriftColumnFamilyDefinition(Map options, ColumnFamily columnFamily) {
        ThriftColumnFamilyDefinitionImpl def = new ThriftColumnFamilyDefinitionImpl();

        Map internalOptions = Maps.newHashMap();
        if (options != null)
            internalOptions.putAll(options);

        internalOptions.put("keyspace", getKeyspaceName());
        
        if (columnFamily != null) {
            internalOptions.put("name", columnFamily.getName());
            if (!internalOptions.containsKey("comparator_type"))
                internalOptions.put("comparator_type", columnFamily.getColumnSerializer().getComparatorType().getTypeName());
            if (!internalOptions.containsKey("key_validation_class"))
                internalOptions.put("key_validation_class", columnFamily.getKeySerializer().getComparatorType().getTypeName());
            if (columnFamily.getDefaultValueSerializer() != null && !internalOptions.containsKey("default_validation_class"))
                internalOptions.put("default_validation_class", columnFamily.getDefaultValueSerializer().getComparatorType().getTypeName());
        }

        def.setFields(internalOptions);
        return def;
    }
    
    /**
     * Convert a Map of options to an internal thrift keyspace definition
     * @param options
     */
    private ThriftKeyspaceDefinitionImpl toThriftKeyspaceDefinition(final Map options) {
        ThriftKeyspaceDefinitionImpl def = new ThriftKeyspaceDefinitionImpl();
        
        Map internalOptions = Maps.newHashMap();
        if (options != null)
            internalOptions.putAll(options);
        
        if (internalOptions.containsKey("name") && !internalOptions.get("name").equals(getKeyspaceName())) {
            throw new RuntimeException(
                    String.format("'name' attribute must match keyspace name. Expected '%s' but got '%s'", 
                                  getKeyspaceName(), internalOptions.get("name")));
        }
        else {
            internalOptions.put("name", getKeyspaceName());
        }
        
        def.setFields(internalOptions); 
        
        return def;
    }

    @Override
    public OperationResult updateKeyspace(final Properties props) throws ConnectionException {
        if (props.containsKey("name") && !props.get("name").equals(getKeyspaceName())) {
            throw new RuntimeException(
                    String.format("'name' attribute must match keyspace name. Expected '%s' but got '%s'", 
                                  getKeyspaceName(), props.get("name")));
        }
        
        final KsDef ksDef;
        try {
            ksDef = ThriftUtils.getThriftObjectFromProperties(KsDef.class, props);
        } catch (Exception e) {
            throw new BadRequestException("Unable to convert properties to KsDef", e);
        }
        ksDef.setName(getKeyspaceName());
        
        return internalUpdateKeyspace(ksDef);
    }

    public OperationResult internalUpdateKeyspace(final KsDef ksDef) throws ConnectionException {
        return connectionPool
                .executeWithFailover(
                        new AbstractOperationImpl(
                                tracerFactory.newTracer(CassandraOperationType.UPDATE_KEYSPACE)) {
                            @Override
                            public SchemaChangeResult internalExecute(Client client, ConnectionContext context) throws Exception {
                                precheckSchemaAgreement(client);
                                return new SchemaChangeResponseImpl().setSchemaId(client.system_update_keyspace(ksDef));
                            }
                        }, RunOnce.get());
    }

    public OperationResult internalCreateKeyspace(final KsDef ksDef) throws ConnectionException {
        if (ksDef.getCf_defs() == null)
            ksDef.setCf_defs(Lists.newArrayList());
        
        return executeDdlOperation(
                        new AbstractOperationImpl(
                                tracerFactory.newTracer(CassandraOperationType.ADD_KEYSPACE)) {
                            @Override
                            public SchemaChangeResult internalExecute(Client client, ConnectionContext context) throws Exception {
                                precheckSchemaAgreement(client);
                                return new SchemaChangeResponseImpl().setSchemaId(client.system_add_keyspace(ksDef));
                            }
                        }, RunOnce.get());
    }

    @Override
    public OperationResult createColumnFamily(final Properties props) throws ConnectionException {
        if (props.containsKey("keyspace") && !props.get("keyspace").equals(getKeyspaceName())) { 
            throw new RuntimeException(
                    String.format("'keyspace' attribute must match keyspace name. Expected '%s' but got '%s'", 
                                  getKeyspaceName(), props.get("keyspace")));
        }
        
        CfDef cfDef;
        try {
            cfDef = ThriftUtils.getThriftObjectFromProperties(CfDef.class, props);
        } catch (Exception e) {
            throw new BadRequestException("Unable to convert properties to CfDef", e);
        }
        cfDef.setKeyspace(getKeyspaceName());
        return internalCreateColumnFamily(cfDef);
    }
    
    private OperationResult internalCreateColumnFamily(final CfDef cfDef) throws ConnectionException {
        return executeDdlOperation(new AbstractKeyspaceOperationImpl(
                                tracerFactory.newTracer(CassandraOperationType.ADD_COLUMN_FAMILY), getKeyspaceName()) {
                            @Override
                            public SchemaChangeResult internalExecute(Client client, ConnectionContext context) throws Exception {
                                precheckSchemaAgreement(client);
                                LOG.info(cfDef.toString());
                                return new SchemaChangeResponseImpl().setSchemaId(client.system_add_column_family(cfDef));
                            }
                        }, RunOnce.get());
    }
    
    private OperationResult internalUpdateColumnFamily(final CfDef cfDef) throws ConnectionException {
        return executeDdlOperation(
                        new AbstractKeyspaceOperationImpl(
                                tracerFactory.newTracer(CassandraOperationType.ADD_COLUMN_FAMILY), getKeyspaceName()) {
                            @Override
                            public SchemaChangeResult internalExecute(Client client, ConnectionContext context) throws Exception {
                                precheckSchemaAgreement(client);
                                return new SchemaChangeResponseImpl().setSchemaId(client.system_update_column_family(cfDef));
                            }
                        }, RunOnce.get());
    }

    @Override
    public OperationResult updateColumnFamily(final Map options) throws ConnectionException  {
        if (options.containsKey("keyspace") && !options.get("keyspace").equals(getKeyspaceName())) {
            throw new RuntimeException(
                    String.format("'keyspace' attribute must match keyspace name. Expected '%s' but got '%s'", 
                                  getKeyspaceName(), options.get("keyspace")));
        }
        
        return connectionPool
                .executeWithFailover(
                        new AbstractKeyspaceOperationImpl(
                                tracerFactory.newTracer(CassandraOperationType.UPDATE_COLUMN_FAMILY), getKeyspaceName()) {
                            @Override
                            public SchemaChangeResult internalExecute(Client client, ConnectionContext context) throws Exception {
                                ThriftColumnFamilyDefinitionImpl def = new ThriftColumnFamilyDefinitionImpl();
                                def.setFields(options);
                                def.setKeyspace(getKeyspaceName());
                                
                                return new SchemaChangeResponseImpl()
                                    .setSchemaId(client.system_update_column_family(def.getThriftColumnFamilyDefinition()));
                            }
                        }, RunOnce.get());
    }
    
    @Override
    public OperationResult updateColumnFamily(final Properties props) throws ConnectionException {
        if (props.containsKey("keyspace") && !props.get("keyspace").equals(getKeyspaceName())) {
            throw new RuntimeException(
                    String.format("'keyspace' attribute must match keyspace name. Expected '%s' but got '%s'", 
                                  getKeyspaceName(), props.get("keyspace")));
        }
        
        return connectionPool
                .executeWithFailover(
                        new AbstractKeyspaceOperationImpl(
                                tracerFactory.newTracer(CassandraOperationType.ADD_COLUMN_FAMILY), getKeyspaceName()) {
                            @Override
                            public SchemaChangeResult internalExecute(Client client, ConnectionContext context) throws Exception {
                                CfDef def = ThriftUtils.getThriftObjectFromProperties(CfDef.class, props);
                                def.setKeyspace(getKeyspaceName());
                                return new SchemaChangeResponseImpl().setSchemaId(client.system_update_column_family(def));
                            }
                        }, RunOnce.get());
    }

    @Override
    public Properties getKeyspaceProperties() throws ConnectionException {
        KeyspaceDefinition ksDef = this.describeKeyspace();
        if (ksDef == null)
            throw new NotFoundException(String.format("Keyspace '%s' not found", getKeyspaceName()));
        
        Properties props = new Properties();
        ThriftKeyspaceDefinitionImpl thriftKsDef = (ThriftKeyspaceDefinitionImpl)ksDef;
        try {
            for (Entry prop : thriftKsDef.getProperties().entrySet()) {
                props.setProperty((String)prop.getKey(), (String) prop.getValue());
            }
        } catch (Exception e) {
            LOG.error(String.format("Error fetching properties for keyspace '%s'", getKeyspaceName()));
        }
        return props;
    }

    @Override
    public Properties getColumnFamilyProperties(String columnFamily) throws ConnectionException {
        KeyspaceDefinition ksDef = this.describeKeyspace();
        ColumnFamilyDefinition cfDef = ksDef.getColumnFamily(columnFamily);
        if (cfDef == null)
            throw new NotFoundException(String.format("Column family '%s' in keyspace '%s' not found", columnFamily, getKeyspaceName()));
        
        Properties props = new Properties();
        ThriftColumnFamilyDefinitionImpl thriftCfDef = (ThriftColumnFamilyDefinitionImpl)cfDef;
        try {
            for (Entry prop : thriftCfDef.getProperties().entrySet()) {
                props.setProperty((String)prop.getKey(), (String) prop.getValue());
            }
        } catch (Exception e) {
            LOG.error("Error processing column family properties");
        }
        return props;    
    }
}