/*******************************************************************************
* 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.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.concurrent.ConcurrentMap;
import org.apache.cassandra.thrift.Cassandra;
import org.apache.cassandra.thrift.Cassandra.Client;
import org.apache.cassandra.thrift.CfDef;
import org.apache.cassandra.thrift.KsDef;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Function;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.netflix.astyanax.AstyanaxConfiguration;
import com.netflix.astyanax.Cluster;
import com.netflix.astyanax.Keyspace;
import com.netflix.astyanax.CassandraOperationType;
import com.netflix.astyanax.KeyspaceTracerFactory;
import com.netflix.astyanax.connectionpool.ConnectionPool;
import com.netflix.astyanax.connectionpool.OperationResult;
import com.netflix.astyanax.connectionpool.ConnectionContext;
import com.netflix.astyanax.connectionpool.exceptions.BadRequestException;
import com.netflix.astyanax.connectionpool.exceptions.ConnectionException;
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.ddl.ColumnDefinition;
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.retry.RunOnce;
import com.netflix.astyanax.thrift.ddl.*;
public class ThriftClusterImpl implements Cluster {
private static final Logger LOG = LoggerFactory.getLogger(ThriftClusterImpl.class);
private static final int MAX_SCHEMA_CHANGE_ATTEMPTS = 6;
private static final int SCHEMA_DISAGREEMENT_BACKOFF = 10000;
private final ConnectionPool<Cassandra.Client> connectionPool;
private final ConcurrentMap<String, Keyspace> keyspaces;
private final AstyanaxConfiguration config;
private final KeyspaceTracerFactory tracerFactory;
public ThriftClusterImpl(
AstyanaxConfiguration config,
ConnectionPool<Cassandra.Client> connectionPool,
KeyspaceTracerFactory tracerFactory) {
this.config = config;
this.connectionPool = connectionPool;
this.tracerFactory = tracerFactory;
this.keyspaces = Maps.newConcurrentMap();
}
@Override
public String describeClusterName() throws ConnectionException {
return connectionPool.executeWithFailover(
new AbstractOperationImpl<String>(tracerFactory.newTracer(CassandraOperationType.DESCRIBE_CLUSTER)) {
@Override
public String internalExecute(Client client, ConnectionContext context) throws Exception {
return client.describe_cluster_name();
}
}, config.getRetryPolicy().duplicate()).getResult();
}
@Override
public String describeSnitch() throws ConnectionException {
return connectionPool.executeWithFailover(
new AbstractOperationImpl<String>(tracerFactory.newTracer(CassandraOperationType.DESCRIBE_SNITCH)) {
@Override
public String internalExecute(Client client, ConnectionContext context) throws Exception {
return client.describe_snitch();
}
}, config.getRetryPolicy().duplicate()).getResult();
}
@Override
public String describePartitioner() throws ConnectionException {
return connectionPool
.executeWithFailover(
new AbstractOperationImpl<String>(
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 Map<String, List<String>> describeSchemaVersions() throws ConnectionException {
return connectionPool.executeWithFailover(
new AbstractOperationImpl<Map<String, List<String>>>(
tracerFactory.newTracer(CassandraOperationType.DESCRIBE_SCHEMA_VERSION)) {
@Override
public Map<String, List<String>> internalExecute(Client client, ConnectionContext context) throws Exception {
return client.describe_schema_versions();
}
}, config.getRetryPolicy().duplicate()).getResult();
}
/**
* Get the version from the cluster
*
* @return
* @throws OperationException
*/
@Override
public String getVersion() throws ConnectionException {
return connectionPool.executeWithFailover(
new AbstractOperationImpl<String>(tracerFactory.newTracer(CassandraOperationType.GET_VERSION)) {
@Override
public String internalExecute(Client client, ConnectionContext state) throws Exception {
return client.describe_version();
}
}, config.getRetryPolicy().duplicate()).getResult();
}
private <K> OperationResult<K> executeSchemaChangeOperation(AbstractOperationImpl<K> op) throws OperationException,
ConnectionException {
int attempt = 0;
do {
try {
return connectionPool.executeWithFailover(op, config.getRetryPolicy().duplicate());
}
catch (SchemaDisagreementException e) {
if (++attempt >= MAX_SCHEMA_CHANGE_ATTEMPTS) {
throw e;
}
try {
Thread.sleep(SCHEMA_DISAGREEMENT_BACKOFF);
}
catch (InterruptedException e1) {
Thread.interrupted();
throw new RuntimeException(e1);
}
}
} while (true);
}
@Override
public List<KeyspaceDefinition> describeKeyspaces() throws ConnectionException {
return connectionPool.executeWithFailover(
new AbstractOperationImpl<List<KeyspaceDefinition>>(
tracerFactory.newTracer(CassandraOperationType.DESCRIBE_KEYSPACES)) {
@Override
public List<KeyspaceDefinition> internalExecute(Client client, ConnectionContext context) throws Exception {
List<KsDef> ksDefs = client.describe_keyspaces();
return Lists.transform(ksDefs, new Function<KsDef, KeyspaceDefinition>() {
@Override
public KeyspaceDefinition apply(KsDef ksDef) {
return new ThriftKeyspaceDefinitionImpl(ksDef);
}
});
}
}, config.getRetryPolicy().duplicate()).getResult();
}
@Override
public KeyspaceDefinition describeKeyspace(String ksName) throws ConnectionException {
List<KeyspaceDefinition> ksDefs = describeKeyspaces();
for (KeyspaceDefinition ksDef : ksDefs) {
if (ksDef.getName().equals(ksName)) {
return ksDef;
}
}
return null;
}
@Override
public Keyspace getKeyspace(String ksName) {
Keyspace keyspace = keyspaces.get(ksName);
if (keyspace == null) {
synchronized (this) {
Keyspace newKeyspace = new ThriftKeyspaceImpl(ksName, this.connectionPool, this.config, tracerFactory);
keyspace = keyspaces.putIfAbsent(ksName, newKeyspace);
if (keyspace == null) {
keyspace = newKeyspace;
}
}
}
return keyspace;
}
@Override
public ColumnFamilyDefinition makeColumnFamilyDefinition() {
return new ThriftColumnFamilyDefinitionImpl();
}
@Override
public OperationResult<SchemaChangeResult> addColumnFamily(final ColumnFamilyDefinition def) throws ConnectionException {
return internalCreateColumnFamily(((ThriftColumnFamilyDefinitionImpl) def)
.getThriftColumnFamilyDefinition());
}
@Override
public OperationResult<SchemaChangeResult> createColumnFamily(final Map<String, Object> options) throws ConnectionException {
final ThriftColumnFamilyDefinitionImpl def = new ThriftColumnFamilyDefinitionImpl();
def.setFields(options);
return internalCreateColumnFamily(def.getThriftColumnFamilyDefinition());
}
@Override
public OperationResult<SchemaChangeResult> createColumnFamily(final Properties props) throws ConnectionException {
final CfDef def;
try {
def = ThriftUtils.getThriftObjectFromProperties(CfDef.class, props);
} catch (Exception e) {
throw new BadRequestException("Error converting properties to CfDef", e);
}
return internalCreateColumnFamily(def);
}
private OperationResult<SchemaChangeResult> internalCreateColumnFamily(final CfDef def) throws ConnectionException {
return executeSchemaChangeOperation(new AbstractKeyspaceOperationImpl<SchemaChangeResult>(
tracerFactory.newTracer(CassandraOperationType.ADD_COLUMN_FAMILY), def.getKeyspace()) {
@Override
public SchemaChangeResult internalExecute(Client client, ConnectionContext context) throws Exception {
precheckSchemaAgreement(client);
return new SchemaChangeResponseImpl()
.setSchemaId(client.system_add_column_family(def));
}
});
}
@Override
public OperationResult<SchemaChangeResult> updateColumnFamily(final ColumnFamilyDefinition def) throws ConnectionException {
return internalColumnFamily(((ThriftColumnFamilyDefinitionImpl) def).getThriftColumnFamilyDefinition());
}
@Override
public OperationResult<SchemaChangeResult> updateColumnFamily(final Map<String, Object> options) throws ConnectionException {
final ThriftColumnFamilyDefinitionImpl def = new ThriftColumnFamilyDefinitionImpl();
def.setFields(options);
return internalColumnFamily(def.getThriftColumnFamilyDefinition());
}
@Override
public OperationResult<SchemaChangeResult> updateColumnFamily(final Properties props) throws ConnectionException {
final CfDef def;
try {
def = ThriftUtils.getThriftObjectFromProperties(CfDef.class, props);
} catch (Exception e) {
throw new BadRequestException("Error converting properties to CfDef", e);
}
return internalColumnFamily(def);
}
private OperationResult<SchemaChangeResult> internalColumnFamily(final CfDef def) throws ConnectionException {
return executeSchemaChangeOperation(new AbstractKeyspaceOperationImpl<SchemaChangeResult>(
tracerFactory.newTracer(CassandraOperationType.UPDATE_COLUMN_FAMILY), def.getKeyspace()) {
@Override
public SchemaChangeResult internalExecute(Client client, ConnectionContext context) throws Exception {
precheckSchemaAgreement(client);
return new SchemaChangeResponseImpl().setSchemaId(client.system_update_column_family(def));
}
});
}
@Override
public KeyspaceDefinition makeKeyspaceDefinition() {
return new ThriftKeyspaceDefinitionImpl();
}
@Override
public OperationResult<SchemaChangeResult> addKeyspace(final KeyspaceDefinition def) throws ConnectionException {
return internalCreateKeyspace(((ThriftKeyspaceDefinitionImpl) def).getThriftKeyspaceDefinition());
}
@Override
public OperationResult<SchemaChangeResult> createKeyspace(final Map<String, Object> options) throws ConnectionException {
final ThriftKeyspaceDefinitionImpl def = new ThriftKeyspaceDefinitionImpl();
def.setFields(options);
return internalCreateKeyspace(def.getThriftKeyspaceDefinition());
}
@Override
public OperationResult<SchemaChangeResult> createKeyspace(final Properties props) throws ConnectionException {
final KsDef def;
try {
def = ThriftUtils.getThriftObjectFromProperties(KsDef.class, props);
if (def.getCf_defs() == null) {
def.setCf_defs(Lists.<CfDef>newArrayList());
}
} catch (Exception e) {
throw new BadRequestException("Error converting properties to KsDef", e);
}
return internalCreateKeyspace(def);
}
private OperationResult<SchemaChangeResult> internalCreateKeyspace(final KsDef def) throws ConnectionException {
return executeSchemaChangeOperation(new AbstractOperationImpl<SchemaChangeResult>(
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(def));
}
});
}
@Override
public OperationResult<SchemaChangeResult> updateKeyspace(final KeyspaceDefinition def) throws ConnectionException {
return internalUpdateKeyspace(((ThriftKeyspaceDefinitionImpl) def).getThriftKeyspaceDefinition());
}
@Override
public OperationResult<SchemaChangeResult> updateKeyspace(final Map<String, Object> options) throws ConnectionException {
final ThriftKeyspaceDefinitionImpl def = new ThriftKeyspaceDefinitionImpl();
try {
def.setFields(options);
} catch (Exception e) {
throw new BadRequestException("Error converting properties to KsDef", e);
}
return internalUpdateKeyspace(def.getThriftKeyspaceDefinition());
}
@Override
public OperationResult<SchemaChangeResult> updateKeyspace(final Properties props) throws ConnectionException {
final KsDef def;
try {
def = ThriftUtils.getThriftObjectFromProperties(KsDef.class, props);
if (def.getCf_defs() == null) {
def.setCf_defs(Lists.<CfDef>newArrayList());
}
} catch (Exception e) {
throw new BadRequestException("Error converting properties to KsDef", e);
}
return internalUpdateKeyspace(def);
}
private OperationResult<SchemaChangeResult> internalUpdateKeyspace(final KsDef def) throws ConnectionException {
return executeSchemaChangeOperation(new AbstractOperationImpl<SchemaChangeResult>(
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(def));
}
});
}
@Override
public ColumnDefinition makeColumnDefinition() {
return new ThriftColumnDefinitionImpl();
}
@Override
public AstyanaxConfiguration getConfig() {
return config;
}
@Override
public OperationResult<SchemaChangeResult> dropColumnFamily(final String keyspaceName, final String columnFamilyName) throws ConnectionException {
return connectionPool
.executeWithFailover(
new AbstractKeyspaceOperationImpl<SchemaChangeResult>(
tracerFactory.newTracer(CassandraOperationType.DROP_COLUMN_FAMILY), keyspaceName) {
@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<SchemaChangeResult> dropKeyspace(final String keyspaceName) throws ConnectionException {
return connectionPool
.executeWithFailover(
new AbstractKeyspaceOperationImpl<SchemaChangeResult>(
tracerFactory.newTracer(CassandraOperationType.DROP_KEYSPACE), keyspaceName) {
@Override
public SchemaChangeResult internalExecute(Client client, ConnectionContext context) throws Exception {
precheckSchemaAgreement(client);
return new SchemaChangeResponseImpl().setSchemaId(client.system_drop_keyspace(keyspaceName));
}
}, RunOnce.get());
}
@Override
public Properties getAllKeyspaceProperties() throws ConnectionException {
List<KeyspaceDefinition> keyspaces = this.describeKeyspaces();
Properties props = new Properties();
for (KeyspaceDefinition ksDef : keyspaces) {
ThriftKeyspaceDefinitionImpl thriftKsDef = (ThriftKeyspaceDefinitionImpl)ksDef;
try {
for (Entry<Object, Object> prop : thriftKsDef.getProperties().entrySet()) {
props.setProperty(ksDef.getName() + "." + prop.getKey(), (String) prop.getValue());
}
} catch (Exception e) {
}
}
return props;
}
@Override
public Properties getKeyspaceProperties(String keyspace) throws ConnectionException {
KeyspaceDefinition ksDef = this.describeKeyspace(keyspace);
if (ksDef == null)
throw new NotFoundException(String.format("Keyspace '%s' not found", keyspace));
Properties props = new Properties();
ThriftKeyspaceDefinitionImpl thriftKsDef = (ThriftKeyspaceDefinitionImpl)ksDef;
try {
for (Entry<Object, Object> 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'", keyspace));
}
return props;
}
@Override
public Properties getColumnFamilyProperties(String keyspace, String columnFamily) throws ConnectionException {
KeyspaceDefinition ksDef = this.describeKeyspace(keyspace);
ColumnFamilyDefinition cfDef = ksDef.getColumnFamily(columnFamily);
if (cfDef == null)
throw new NotFoundException(String.format("Column family '%s' in keyspace '%s' not found", columnFamily, keyspace));
Properties props = new Properties();
ThriftColumnFamilyDefinitionImpl thriftCfDef = (ThriftColumnFamilyDefinitionImpl)cfDef;
try {
for (Entry<Object, Object> prop : thriftCfDef.getProperties().entrySet()) {
props.setProperty((String)prop.getKey(), (String) prop.getValue());
}
} catch (Exception e) {
LOG.error("Error processing column family properties");
}
return props;
}
/**
* 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<String, List<String>> schemas = client.describe_schema_versions();
if (schemas.size() > 1) {
throw new SchemaDisagreementException("Can't change schema due to pending schema agreement");
}
}
}