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

com.thinkaurelius.titan.diskstorage.cassandra.thrift.CassandraThriftStoreManager Maven / Gradle / Ivy

The newest version!
package com.thinkaurelius.titan.diskstorage.cassandra.thrift;

import static com.thinkaurelius.titan.diskstorage.cassandra.CassandraTransaction.getTx;

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import com.thinkaurelius.titan.diskstorage.EntryMetaData;
import com.thinkaurelius.titan.diskstorage.*;
import com.thinkaurelius.titan.diskstorage.cassandra.utils.CassandraHelper;
import com.thinkaurelius.titan.diskstorage.configuration.ConfigElement;
import com.thinkaurelius.titan.diskstorage.configuration.ConfigNamespace;
import com.thinkaurelius.titan.diskstorage.configuration.ConfigOption;
import com.thinkaurelius.titan.diskstorage.configuration.Configuration;
import com.thinkaurelius.titan.diskstorage.keycolumnvalue.KeyRange;
import com.thinkaurelius.titan.graphdb.configuration.PreInitializeConfigOptions;
import com.thinkaurelius.titan.util.system.NetworkUtil;

import org.apache.cassandra.dht.AbstractByteOrderedPartitioner;
import org.apache.cassandra.dht.ByteOrderedPartitioner;
import org.apache.cassandra.dht.IPartitioner;
import org.apache.cassandra.dht.Token;
import org.apache.cassandra.thrift.*;
import org.apache.cassandra.utils.FBUtilities;
import org.apache.commons.pool.impl.GenericKeyedObjectPool;
import org.apache.thrift.TException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.thinkaurelius.titan.diskstorage.BackendException;
import com.thinkaurelius.titan.diskstorage.cassandra.AbstractCassandraStoreManager;
import com.thinkaurelius.titan.diskstorage.cassandra.thrift.thriftpool.CTConnection;
import com.thinkaurelius.titan.diskstorage.cassandra.thrift.thriftpool.CTConnectionFactory;
import com.thinkaurelius.titan.diskstorage.cassandra.thrift.thriftpool.CTConnectionPool;
import com.thinkaurelius.titan.diskstorage.keycolumnvalue.KCVMutation;
import com.thinkaurelius.titan.diskstorage.keycolumnvalue.StoreTransaction;
import com.thinkaurelius.titan.graphdb.configuration.GraphDatabaseConfiguration;

import static com.thinkaurelius.titan.diskstorage.configuration.ConfigOption.disallowEmpty;

/**
 * This class creates {@see CassandraThriftKeyColumnValueStore}s and
 * handles Cassandra-backed allocation of vertex IDs for Titan (when so
 * configured).
 *
 * @author Dan LaRocque 
 */
@PreInitializeConfigOptions
public class CassandraThriftStoreManager extends AbstractCassandraStoreManager {

    public enum PoolExhaustedAction {
        BLOCK(GenericKeyedObjectPool.WHEN_EXHAUSTED_BLOCK),
        FAIL(GenericKeyedObjectPool.WHEN_EXHAUSTED_FAIL),
        GROW(GenericKeyedObjectPool.WHEN_EXHAUSTED_GROW);

        private final byte b;

        PoolExhaustedAction(byte b) {
            this.b = b;
        }

        public byte getByte() {
            return b;
        }
    }

    private static final Logger log = LoggerFactory.getLogger(CassandraThriftStoreManager.class);

    public static final ConfigNamespace THRIFT_NS =
            new ConfigNamespace(AbstractCassandraStoreManager.CASSANDRA_NS, "thrift",
                    "Options for Titan's own Thrift Cassandra backend");

    public static final ConfigNamespace CPOOL_NS =
            new ConfigNamespace(THRIFT_NS, "cpool", "Options for the Apache commons-pool connection manager");

    public static final ConfigOption CPOOL_WHEN_EXHAUSTED =
            new ConfigOption<>(CPOOL_NS, "when-exhausted",
            "What to do when clients concurrently request more active connections than are allowed " +
            "by the pool.  The value must be one of BLOCK, FAIL, or GROW.",
            ConfigOption.Type.MASKABLE, String.class, PoolExhaustedAction.BLOCK.toString(),
            disallowEmpty(String.class));

    public static final ConfigOption CPOOL_MAX_TOTAL =
            new ConfigOption(CPOOL_NS, "max-total",
            "Max number of allowed Thrift connections, idle or active (-1 to leave undefined)",
            ConfigOption.Type.MASKABLE, -1);

    public static final ConfigOption CPOOL_MAX_ACTIVE =
            new ConfigOption(CPOOL_NS, "max-active",
            "Maximum number of concurrently in-use connections (-1 to leave undefined)",
            ConfigOption.Type.MASKABLE, 16);

    public static final ConfigOption CPOOL_MAX_IDLE =
            new ConfigOption(CPOOL_NS, "max-idle",
            "Maximum number of concurrently idle connections (-1 to leave undefined)",
            ConfigOption.Type.MASKABLE, 4);

    public static final ConfigOption CPOOL_MIN_IDLE =
            new ConfigOption(CPOOL_NS, "min-idle",
            "Minimum number of idle connections the pool attempts to maintain",
            ConfigOption.Type.MASKABLE, 0);

    // Wart: allowing -1 like commons-pool's convention precludes using StandardDuration
    public static final ConfigOption CPOOL_MAX_WAIT =
            new ConfigOption(CPOOL_NS, "max-wait",
            "Maximum number of milliseconds to block when " + ConfigElement.getPath(CPOOL_WHEN_EXHAUSTED) +
            " is set to BLOCK.  Has no effect when set to actions besides BLOCK.  Set to -1 to wait indefinitely.",
            ConfigOption.Type.MASKABLE, -1L);

    // Wart: allowing -1 like commons-pool's convention precludes using StandardDuration
    public static final ConfigOption CPOOL_EVICTOR_PERIOD =
            new ConfigOption(CPOOL_NS, "evictor-period",
            "Approximate number of milliseconds between runs of the idle connection evictor.  " +
            "Set to -1 to never run the idle connection evictor.",
            ConfigOption.Type.MASKABLE, 30L * 1000L);

    // Wart: allowing -1 like commons-pool's convention precludes using StandardDuration
    public static final ConfigOption CPOOL_MIN_EVICTABLE_IDLE_TIME =
            new ConfigOption(CPOOL_NS, "min-evictable-idle-time",
            "Minimum number of milliseconds a connection must be idle before it is eligible for " +
            "eviction.  See also " + ConfigElement.getPath(CPOOL_EVICTOR_PERIOD) + ".  Set to -1 to never evict " +
            "idle connections.", ConfigOption.Type.MASKABLE, 60L * 1000L);

    public static final ConfigOption CPOOL_IDLE_TESTS =
            new ConfigOption(CPOOL_NS, "idle-test",
            "Whether the idle connection evictor validates idle connections and drops those that fail to validate",
            ConfigOption.Type.MASKABLE, false);

    public static final ConfigOption CPOOL_IDLE_TESTS_PER_EVICTION_RUN =
            new ConfigOption(CPOOL_NS, "idle-tests-per-eviction-run",
            "When the value is negative, e.g. -n, roughly one nth of the idle connections are tested per run.  " +
            "When the value is positive, e.g. n, the min(idle-count, n) connections are tested per run.",
            ConfigOption.Type.MASKABLE, 0);


    private final Map openStores;
    private final CTConnectionPool pool;
    private final Deployment deployment;

    public CassandraThriftStoreManager(Configuration config) throws BackendException {
        super(config);

        /*
         * This is eventually passed to Thrift's TSocket constructor. The
         * constructor parameter is of type int.
         */
        int thriftTimeoutMS = (int)config.get(GraphDatabaseConfiguration.CONNECTION_TIMEOUT).toMillis();

        CTConnectionFactory.Config factoryConfig = new CTConnectionFactory.Config(hostnames, port, username, password)
                                                                            .setTimeoutMS(thriftTimeoutMS)
                                                                            .setFrameSize(thriftFrameSizeBytes);

        if (config.get(SSL_ENABLED)) {
            factoryConfig.setSSLTruststoreLocation(config.get(SSL_TRUSTSTORE_LOCATION));
            factoryConfig.setSSLTruststorePassword(config.get(SSL_TRUSTSTORE_PASSWORD));
        }

        final PoolExhaustedAction poolExhaustedAction = ConfigOption.getEnumValue(
                config.get(CPOOL_WHEN_EXHAUSTED), PoolExhaustedAction.class);

        CTConnectionPool p = new CTConnectionPool(factoryConfig.build());
        p.setTestOnBorrow(true);
        p.setTestOnReturn(true);
        p.setTestWhileIdle(config.get(CPOOL_IDLE_TESTS));
        p.setNumTestsPerEvictionRun(config.get(CPOOL_IDLE_TESTS_PER_EVICTION_RUN));
        p.setWhenExhaustedAction(poolExhaustedAction.getByte());
        p.setMaxActive(config.get(CPOOL_MAX_ACTIVE));
        p.setMaxTotal(config.get(CPOOL_MAX_TOTAL)); // maxTotal limits active + idle
        p.setMaxIdle(config.get(CPOOL_MAX_IDLE));
        p.setMinIdle(config.get(CPOOL_MIN_IDLE));
        p.setMaxWait(config.get(CPOOL_MAX_WAIT));
        p.setTimeBetweenEvictionRunsMillis(config.get(CPOOL_EVICTOR_PERIOD));
        p.setMinEvictableIdleTimeMillis(config.get(CPOOL_MIN_EVICTABLE_IDLE_TIME));
        this.pool = p;

        this.openStores = new HashMap();

        // Only watch the ring and change endpoints with BOP
        if (getCassandraPartitioner() instanceof ByteOrderedPartitioner) {
            deployment = (hostnames.length == 1)// mark deployment as local only in case we have byte ordered partitioner and local connection
                          ? (NetworkUtil.isLocalConnection(hostnames[0])) ? Deployment.LOCAL : Deployment.REMOTE
                          : Deployment.REMOTE;
        } else {
            deployment = Deployment.REMOTE;
        }
    }

    @Override
    public Deployment getDeployment() {
        return deployment;
    }

    @Override
    @SuppressWarnings("unchecked")
    public IPartitioner getCassandraPartitioner() throws BackendException {
        CTConnection conn = null;
        try {
            conn = pool.borrowObject(SYSTEM_KS);
            return FBUtilities.newPartitioner(conn.getClient().describe_partitioner());
        } catch (Exception e) {
            throw new TemporaryBackendException(e);
        } finally {
            pool.returnObjectUnsafe(SYSTEM_KS, conn);
        }
    }

    @Override
    public String toString() {
        return "thriftCassandra" + super.toString();
    }

    @Override
    public void close() throws BackendException {
        openStores.clear();
        closePool();
    }

    @Override
    public void mutateMany(Map> mutations, StoreTransaction txh) throws BackendException {
        Preconditions.checkNotNull(mutations);

        final MaskedTimestamp commitTime = new MaskedTimestamp(txh);

        ConsistencyLevel consistency = getTx(txh).getWriteConsistencyLevel().getThrift();

        // Generate Thrift-compatible batch_mutate() datastructure
        // key -> cf -> cassmutation
        int size = 0;
        for (Map mutation : mutations.values()) size += mutation.size();
        Map>> batch =
                new HashMap>>(size);


        for (Map.Entry> keyMutation : mutations.entrySet()) {
            String columnFamily = keyMutation.getKey();
            for (Map.Entry mutEntry : keyMutation.getValue().entrySet()) {
                ByteBuffer keyBB = mutEntry.getKey().asByteBuffer();

                // Get or create the single Cassandra Mutation object responsible for this key
                Map> cfmutation = batch.get(keyBB);
                if (cfmutation == null) {
                    cfmutation = new HashMap>(3); // Most mutations only modify the edgeStore and indexStore
                    batch.put(keyBB, cfmutation);
                }

                KCVMutation mutation = mutEntry.getValue();
                List thriftMutation =
                        new ArrayList(mutations.size());

                if (mutation.hasDeletions()) {
                    for (StaticBuffer buf : mutation.getDeletions()) {
                        Deletion d = new Deletion();
                        SlicePredicate sp = new SlicePredicate();
                        sp.addToColumn_names(buf.as(StaticBuffer.BB_FACTORY));
                        d.setPredicate(sp);
                        d.setTimestamp(commitTime.getDeletionTime(times));
                        org.apache.cassandra.thrift.Mutation m = new org.apache.cassandra.thrift.Mutation();
                        m.setDeletion(d);
                        thriftMutation.add(m);
                    }
                }

                if (mutation.hasAdditions()) {
                    for (Entry ent : mutation.getAdditions()) {
                        ColumnOrSuperColumn cosc = new ColumnOrSuperColumn();
                        Column column = new Column(ent.getColumnAs(StaticBuffer.BB_FACTORY));
                        column.setValue(ent.getValueAs(StaticBuffer.BB_FACTORY));

                        column.setTimestamp(commitTime.getAdditionTime(times));

                        Integer ttl = (Integer) ent.getMetaData().get(EntryMetaData.TTL);
                        if (null != ttl && ttl > 0) {
                            column.setTtl(ttl);
                        }

                        cosc.setColumn(column);
                        org.apache.cassandra.thrift.Mutation m = new org.apache.cassandra.thrift.Mutation();
                        m.setColumn_or_supercolumn(cosc);
                        thriftMutation.add(m);
                    }
                }

                cfmutation.put(columnFamily, thriftMutation);
            }
        }

        CTConnection conn = null;
        try {
            conn = pool.borrowObject(keySpaceName);
            Cassandra.Client client = conn.getClient();
            if (atomicBatch) {
                client.atomic_batch_mutate(batch, consistency);
            } else {
                client.batch_mutate(batch, consistency);
            }
        } catch (Exception ex) {
            throw CassandraThriftKeyColumnValueStore.convertException(ex);
        } finally {
            pool.returnObjectUnsafe(keySpaceName, conn);
        }

        sleepAfterWrite(txh, commitTime);
    }

    @Override // TODO: *BIG FAT WARNING* 'synchronized is always *bad*, change openStores to use ConcurrentLinkedHashMap
    public synchronized CassandraThriftKeyColumnValueStore openDatabase(final String name, StoreMetaData.Container metaData) throws BackendException {
        if (openStores.containsKey(name))
            return openStores.get(name);

        ensureColumnFamilyExists(keySpaceName, name);

        CassandraThriftKeyColumnValueStore store = new CassandraThriftKeyColumnValueStore(keySpaceName, name, this, pool);
        openStores.put(name, store);
        return store;
    }

    @Override
    public List getLocalKeyPartition() throws BackendException {
        CTConnection conn = null;
        IPartitioner partitioner = getCassandraPartitioner();

        if (!(partitioner instanceof AbstractByteOrderedPartitioner))
            throw new UnsupportedOperationException("getLocalKeyPartition() only supported by byte ordered partitioner.");

        Token.TokenFactory tokenFactory = partitioner.getTokenFactory();

        try {
            // Resist the temptation to describe SYSTEM_KS.  It has no ring.
            // Instead, we'll create our own keyspace (or check that it exists), then describe it.
            ensureKeyspaceExists(keySpaceName);

            conn = pool.borrowObject(keySpaceName);
            List ranges  = conn.getClient().describe_ring(keySpaceName);
            List keyRanges = new ArrayList(ranges.size());

            for (TokenRange range : ranges) {
                if (!NetworkUtil.hasLocalAddress(range.endpoints))
                    continue;

                keyRanges.add(CassandraHelper.transformRange(tokenFactory.fromString(range.start_token), tokenFactory.fromString(range.end_token)));
            }

            return keyRanges;
        } catch (Exception e) {
            throw CassandraThriftKeyColumnValueStore.convertException(e);
        } finally {
            pool.returnObjectUnsafe(keySpaceName, conn);
        }
    }

    /**
     * Connect to Cassandra via Thrift on the specified host and port and attempt to truncate the named keyspace.
     * 

* This is a utility method intended mainly for testing. It is * equivalent to issuing 'truncate ' for each of the column families in keyspace using * the cassandra-cli tool. *

* Using truncate is better for a number of reasons, most significantly because it doesn't * involve any schema modifications which can take time to propagate across the cluster such * leaves nodes in the inconsistent state and could result in read/write failures. * Any schema modifications are discouraged until there is no traffic to Keyspace or ColumnFamilies. * * @throws com.thinkaurelius.titan.diskstorage.BackendException if any checked Thrift or UnknownHostException is thrown in the body of this method */ public void clearStorage() throws BackendException { openStores.clear(); final String lp = "ClearStorage: "; // "log prefix" /* * log4j is capable of automatically writing the name of a method that * generated a log message, but the docs warn that "generating caller * location information is extremely slow and should be avoided unless * execution speed is not an issue." */ CTConnection conn = null; try { conn = pool.borrowObject(SYSTEM_KS); Cassandra.Client client = conn.getClient(); KsDef ksDef; try { client.set_keyspace(keySpaceName); ksDef = client.describe_keyspace(keySpaceName); } catch (NotFoundException e) { log.debug(lp + "Keyspace {} does not exist, not attempting to truncate.", keySpaceName); return; } catch (InvalidRequestException e) { log.debug(lp + "InvalidRequestException when attempting to describe keyspace {}, not attempting to truncate.", keySpaceName); return; } if (null == ksDef) { log.debug(lp + "Received null KsDef for keyspace {}; not truncating its CFs", keySpaceName); return; } List cfDefs = ksDef.getCf_defs(); if (null == cfDefs) { log.debug(lp + "Received empty CfDef list for keyspace {}; not truncating CFs", keySpaceName); return; } for (CfDef cfDef : ksDef.getCf_defs()) { client.truncate(cfDef.name); log.info(lp + "Truncated CF {} in keyspace {}", cfDef.name, keySpaceName); } /* * Clearing the CTConnectionPool is unnecessary. This method * removes no keyspaces. All open Cassandra connections will * remain valid. */ } catch (Exception e) { throw new TemporaryBackendException(e); } finally { if (conn != null && conn.getClient() != null) { try { conn.getClient().set_keyspace(SYSTEM_KS); } catch (InvalidRequestException e) { log.warn("Failed to reset keyspace", e); } catch (TException e) { log.warn("Failed to reset keyspace", e); } } pool.returnObjectUnsafe(SYSTEM_KS, conn); } } private KsDef ensureKeyspaceExists(String keyspaceName) throws TException, BackendException { CTConnection connection = null; try { connection = pool.borrowObject(SYSTEM_KS); Cassandra.Client client = connection.getClient(); try { // Side effect: throws Exception if keyspaceName doesn't exist client.set_keyspace(keyspaceName); // Don't remove client.set_keyspace(SYSTEM_KS); log.debug("Found existing keyspace {}", keyspaceName); } catch (InvalidRequestException e) { // Keyspace didn't exist; create it log.debug("Creating keyspace {}...", keyspaceName); KsDef ksdef = new KsDef().setName(keyspaceName) .setCf_defs(new LinkedList()) // cannot be null but can be empty .setStrategy_class(storageConfig.get(REPLICATION_STRATEGY)) .setStrategy_options(strategyOptions); client.set_keyspace(SYSTEM_KS); try { client.system_add_keyspace(ksdef); retrySetKeyspace(keyspaceName, client); log.debug("Created keyspace {}", keyspaceName); } catch (InvalidRequestException ire) { log.error("system_add_keyspace failed for keyspace=" + keyspaceName, ire); throw ire; } } return client.describe_keyspace(keyspaceName); } catch (Exception e) { throw new TemporaryBackendException(e); } finally { pool.returnObjectUnsafe(SYSTEM_KS, connection); } } private void retrySetKeyspace(String ksName, Cassandra.Client client) throws BackendException { final long end = System.currentTimeMillis() + (60L * 1000L); while (System.currentTimeMillis() <= end) { try { client.set_keyspace(ksName); return; } catch (Exception e) { log.warn("Exception when changing to keyspace {} after creating it", ksName, e); try { Thread.sleep(1000L); } catch (InterruptedException ie) { throw new PermanentBackendException("Unexpected interrupt (shutting down?)", ie); } } } throw new PermanentBackendException("Could change to keyspace " + ksName + " after creating it"); } private void ensureColumnFamilyExists(String ksName, String cfName) throws BackendException { ensureColumnFamilyExists(ksName, cfName, "org.apache.cassandra.db.marshal.BytesType"); } private void ensureColumnFamilyExists(String ksName, String cfName, String comparator) throws BackendException { CTConnection conn = null; try { KsDef keyspaceDef = ensureKeyspaceExists(ksName); conn = pool.borrowObject(ksName); Cassandra.Client client = conn.getClient(); log.debug("Looking up metadata on keyspace {}...", ksName); boolean foundColumnFamily = false; for (CfDef cfDef : keyspaceDef.getCf_defs()) { String curCfName = cfDef.getName(); if (curCfName.equals(cfName)) foundColumnFamily = true; } if (!foundColumnFamily) { createColumnFamily(client, ksName, cfName, comparator); } else { log.debug("Keyspace {} and ColumnFamily {} were found.", ksName, cfName); } } catch (SchemaDisagreementException e) { throw new TemporaryBackendException(e); } catch (Exception e) { throw new PermanentBackendException(e); } finally { pool.returnObjectUnsafe(ksName, conn); } } private void createColumnFamily(Cassandra.Client client, String ksName, String cfName, String comparator) throws BackendException { CfDef createColumnFamily = new CfDef(); createColumnFamily.setName(cfName); createColumnFamily.setKeyspace(ksName); createColumnFamily.setComparator_type(comparator); ImmutableMap.Builder compressionOptions = new ImmutableMap.Builder(); if (compressionEnabled) { compressionOptions.put("sstable_compression", compressionClass) .put("chunk_length_kb", Integer.toString(compressionChunkSizeKB)); } createColumnFamily.setCompression_options(compressionOptions.build()); // Hard-coded caching settings if (cfName.startsWith(Backend.EDGESTORE_NAME)) { createColumnFamily.setCaching("keys_only"); } else if (cfName.startsWith(Backend.INDEXSTORE_NAME)) { createColumnFamily.setCaching("rows_only"); } log.debug("Adding column family {} to keyspace {}...", cfName, ksName); try { client.system_add_column_family(createColumnFamily); } catch (SchemaDisagreementException e) { throw new TemporaryBackendException("Error in setting up column family", e); } catch (Exception e) { throw new PermanentBackendException(e); } log.debug("Added column family {} to keyspace {}.", cfName, ksName); } @Override public Map getCompressionOptions(String cf) throws BackendException { CTConnection conn = null; Map result = null; try { conn = pool.borrowObject(keySpaceName); Cassandra.Client client = conn.getClient(); KsDef ksDef = client.describe_keyspace(keySpaceName); for (CfDef cfDef : ksDef.getCf_defs()) { if (null != cfDef && cfDef.getName().equals(cf)) { result = cfDef.getCompression_options(); break; } } return result; } catch (InvalidRequestException e) { log.debug("Keyspace {} does not exist", keySpaceName); return null; } catch (Exception e) { throw new TemporaryBackendException(e); } finally { pool.returnObjectUnsafe(keySpaceName, conn); } } private void closePool() { /* * pool.close() does not affect borrowed connections. * * Connections currently borrowed by some thread which are * talking to the old host will eventually be destroyed by * CTConnectionFactory#validateObject() returning false when * those connections are returned to the pool. */ try { pool.close(); log.info("Closed Thrift connection pooler."); } catch (Exception e) { log.warn("Failed to close connection pooler. " + "We might be leaking Cassandra connections.", e); // There's still hope: CTConnectionFactory#validateObject() // will be called on borrow() and might tear down the // connections that close() failed to tear down } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy