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

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

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

import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.thinkaurelius.titan.diskstorage.*;
import com.thinkaurelius.titan.diskstorage.cassandra.thrift.thriftpool.CTConnection;
import com.thinkaurelius.titan.diskstorage.cassandra.thrift.thriftpool.CTConnectionPool;
import com.thinkaurelius.titan.diskstorage.cassandra.utils.CassandraHelper;
import com.thinkaurelius.titan.diskstorage.keycolumnvalue.*;
import com.thinkaurelius.titan.diskstorage.util.*;
import org.apache.cassandra.dht.*;
import org.apache.cassandra.thrift.*;
import org.apache.cassandra.thrift.ConsistencyLevel;
import org.apache.commons.lang.ArrayUtils;
import org.apache.thrift.TException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nullable;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

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

/**
 * A Titan {@code KeyColumnValueStore} backed by Cassandra.
 * This uses the Cassandra Thrift API.
 *
 * @author Dan LaRocque 
 * @see CassandraThriftStoreManager
 */
public class CassandraThriftKeyColumnValueStore implements KeyColumnValueStore {

    private static final Logger logger =
            LoggerFactory.getLogger(CassandraThriftKeyColumnValueStore.class);

    private static final Pattern BROKEN_BYTE_TOKEN_PATTERN = Pattern.compile("^Token\\(bytes\\[(.+)\\]\\)$");

    // Cassandra access
    private final CassandraThriftStoreManager storeManager;
    private final String keyspace;
    private final String columnFamily;
    private final CTConnectionPool pool;
    private final ThriftGetter entryGetter;

    public CassandraThriftKeyColumnValueStore(String keyspace, String columnFamily, CassandraThriftStoreManager storeManager,
                                              CTConnectionPool pool) {
        this.storeManager = storeManager;
        this.keyspace = keyspace;
        this.columnFamily = columnFamily;
        this.pool = pool;
        this.entryGetter = new ThriftGetter(storeManager.getMetaDataSchema(columnFamily));
    }

    /**
     * Call Cassandra's Thrift get_slice() method.
     * 

* When columnEnd equals columnStart and either startInclusive * or endInclusive is false (or both are false), then this * method returns an empty list without making any Thrift calls. *

* If columnEnd = columnStart + 1, and both startInclusive and * startExclusive are false, then the arguments effectively form * an empty interval. In this case, as in the one previous, * an empty list is returned. However, it may not necessarily * be handled efficiently; a Thrift call might still be made * before returning the empty list. * * @throws com.thinkaurelius.titan.diskstorage.BackendException * when columnEnd < columnStart */ @Override public EntryList getSlice(KeySliceQuery query, StoreTransaction txh) throws BackendException { Map result = getNamesSlice(query.getKey(), query, txh); return Iterables.getOnlyElement(result.values(), EntryList.EMPTY_LIST); } @Override public Map getSlice(List keys, SliceQuery query, StoreTransaction txh) throws BackendException { return getNamesSlice(keys, query, txh); } public Map getNamesSlice(StaticBuffer key, SliceQuery query, StoreTransaction txh) throws BackendException { return getNamesSlice(ImmutableList.of(key),query,txh); } public Map getNamesSlice(List keys, SliceQuery query, StoreTransaction txh) throws BackendException { ColumnParent parent = new ColumnParent(columnFamily); /* * Cassandra cannot handle columnStart = columnEnd. * Cassandra's Thrift getSlice() throws InvalidRequestException * if columnStart = columnEnd. */ if (query.getSliceStart().compareTo(query.getSliceEnd()) >= 0) { // Check for invalid arguments where columnEnd < columnStart if (query.getSliceEnd().compareTo(query.getSliceStart())<0) { throw new PermanentBackendException("columnStart=" + query.getSliceStart() + " is greater than columnEnd=" + query.getSliceEnd() + ". " + "columnStart must be less than or equal to columnEnd"); } if (0 != query.getSliceStart().length() && 0 != query.getSliceEnd().length()) { logger.debug("Return empty list due to columnEnd==columnStart and neither empty"); return KCVSUtil.emptyResults(keys); } } assert query.getSliceStart().compareTo(query.getSliceEnd()) < 0; ConsistencyLevel consistency = getTx(txh).getReadConsistencyLevel().getThrift(); SlicePredicate predicate = new SlicePredicate(); SliceRange range = new SliceRange(); range.setCount(query.getLimit() + (query.hasLimit()?1:0)); //Add one for potentially removed last column range.setStart(query.getSliceStart().asByteBuffer()); range.setFinish(query.getSliceEnd().asByteBuffer()); predicate.setSlice_range(range); CTConnection conn = null; try { conn = pool.borrowObject(keyspace); Cassandra.Client client = conn.getClient(); Map> rows = client.multiget_slice(CassandraHelper.convert(keys), parent, predicate, consistency); /* * The final size of the "result" List may be at most rows.size(). * However, "result" could also be up to two elements smaller than * rows.size(), depending on startInclusive and endInclusive */ Map results = new HashMap(); for (ByteBuffer key : rows.keySet()) { results.put(StaticArrayBuffer.of(key), CassandraHelper.makeEntryList(rows.get(key), entryGetter, query.getSliceEnd(), query.getLimit())); } return results; } catch (Exception e) { throw convertException(e); } finally { pool.returnObjectUnsafe(keyspace, conn); } } private static class ThriftGetter implements StaticArrayEntry.GetColVal { private final EntryMetaData[] schema; private ThriftGetter(EntryMetaData[] schema) { this.schema = schema; } @Override public ByteBuffer getColumn(ColumnOrSuperColumn element) { return element.getColumn().bufferForName(); } @Override public ByteBuffer getValue(ColumnOrSuperColumn element) { return element.getColumn().bufferForValue(); } @Override public EntryMetaData[] getMetaSchema(ColumnOrSuperColumn element) { return schema; } @Override public Object getMetaData(ColumnOrSuperColumn element, EntryMetaData meta) { switch(meta) { case TIMESTAMP: return element.getColumn().getTimestamp(); case TTL: return element.getColumn().getTtl(); default: throw new UnsupportedOperationException("Unsupported meta data: " + meta); } } } @Override public void close() { // Do nothing } @Override public void acquireLock(StaticBuffer key, StaticBuffer column, StaticBuffer expectedValue, StoreTransaction txh) throws BackendException { throw new UnsupportedOperationException(); } @Override public KeyIterator getKeys(@Nullable SliceQuery sliceQuery, StoreTransaction txh) throws BackendException { final IPartitioner partitioner = storeManager.getCassandraPartitioner(); if (!(partitioner instanceof RandomPartitioner) && !(partitioner instanceof Murmur3Partitioner)) throw new PermanentBackendException("This operation is only allowed when random partitioner (md5 or murmur3) is used."); try { return new AllTokensIterator(partitioner, sliceQuery, storeManager.getPageSize()); } catch (Exception e) { throw convertException(e); } } @Override public KeyIterator getKeys(KeyRangeQuery keyRangeQuery, StoreTransaction txh) throws BackendException { final IPartitioner partitioner = storeManager.getCassandraPartitioner(); // see rant about the reason of this limitation in Astyanax implementation of this method. if (!(partitioner instanceof AbstractByteOrderedPartitioner)) throw new PermanentBackendException("This operation is only allowed when byte-ordered partitioner is used."); try { return new KeyRangeIterator(partitioner, keyRangeQuery, storeManager.getPageSize(), keyRangeQuery.getKeyStart().asByteBuffer(), keyRangeQuery.getKeyEnd().asByteBuffer()); } catch (Exception e) { throw convertException(e); } } @Override public String getName() { return columnFamily; } @Override public void mutate(StaticBuffer key, List additions, List deletions, StoreTransaction txh) throws BackendException { Map mutations = ImmutableMap.of(key, new KCVMutation(additions, deletions)); mutateMany(mutations, txh); } public void mutateMany(Map mutations, StoreTransaction txh) throws BackendException { storeManager.mutateMany(ImmutableMap.of(columnFamily, mutations), txh); } static BackendException convertException(Throwable e) { if (e instanceof TException) { return new PermanentBackendException(e); } else if (e instanceof TimedOutException) { return new TemporaryBackendException(e); } else if (e instanceof UnavailableException) { return new TemporaryBackendException(e); } else if (e instanceof InvalidRequestException) { return new PermanentBackendException(e); } else { return new PermanentBackendException(e); } } @Override public String toString() { return "CassandraThriftKeyColumnValueStore[ks=" + keyspace + ", cf=" + columnFamily + "]"; } private List getKeySlice(ByteBuffer startKey, ByteBuffer endKey, SliceQuery columnSlice, int count) throws BackendException { return getRangeSlices(new org.apache.cassandra.thrift.KeyRange().setStart_key(startKey).setEnd_key(endKey).setCount(count), columnSlice); } private List getTokenSlice(T startToken, T endToken, SliceQuery sliceQuery, int count) throws BackendException { String st = sanitizeBrokenByteToken(startToken); String et = sanitizeBrokenByteToken(endToken); org.apache.cassandra.thrift.KeyRange kr = new org.apache.cassandra.thrift.KeyRange().setStart_token(st).setEnd_token(et).setCount(count); return getRangeSlices(kr, sliceQuery); } private String sanitizeBrokenByteToken(Token tok) { /* * Background: https://issues.apache.org/jira/browse/CASSANDRA-5566 * * This check is useful for compatibility with Cassandra server versions * 1.2.4 and earlier. */ String st = tok.toString(); if (!(tok instanceof BytesToken)) return st; // Do a cheap 1-character startsWith before unleashing the regex if (st.startsWith("T")) { Matcher m = BROKEN_BYTE_TOKEN_PATTERN.matcher(st); if (!m.matches()) { logger.warn("Unknown token string format: \"{}\"", st); } else { String old = st; st = m.group(1); logger.debug("Rewrote token string: \"{}\" -> \"{}\"", old, st); } } return st; } private List getRangeSlices(org.apache.cassandra.thrift.KeyRange keyRange, @Nullable SliceQuery sliceQuery) throws BackendException { SliceRange sliceRange = new SliceRange(); if (sliceQuery == null) { sliceRange.setStart(ArrayUtils.EMPTY_BYTE_ARRAY) .setFinish(ArrayUtils.EMPTY_BYTE_ARRAY) .setCount(5); } else { sliceRange.setStart(sliceQuery.getSliceStart().asByteBuffer()) .setFinish(sliceQuery.getSliceEnd().asByteBuffer()) .setCount((sliceQuery.hasLimit()) ? sliceQuery.getLimit() : Integer.MAX_VALUE); } CTConnection connection = null; try { connection = pool.borrowObject(keyspace); List slices = connection.getClient().get_range_slices(new ColumnParent(columnFamily), new SlicePredicate() .setSlice_range(sliceRange), keyRange, ConsistencyLevel.QUORUM); for (KeySlice s : slices) { logger.debug("Key {}", ByteBufferUtil.toString(s.key, "-")); } /* Note: we need to fetch columns for each row as well to remove "range ghosts" */ List result = new ArrayList<>(slices.size()); KeyIterationPredicate pred = new KeyIterationPredicate(); for (KeySlice ks : slices) if (pred.apply(ks)) result.add(ks); return result; } catch (Exception e) { throw convertException(e); } finally { if (connection != null) pool.returnObjectUnsafe(keyspace, connection); } } private static class KeyIterationPredicate implements Predicate { @Override public boolean apply(@Nullable KeySlice row) { return (row != null) && row.getColumns().size() > 0; } } /** * Slices rows and columns using tokens. Recall that the partitioner turns * keys into tokens. For instance, under RandomPartitioner, tokens are the * MD5 hashes of keys. */ public class AbstractBufferedRowIter implements KeyIterator { private final int pageSize; private final SliceQuery columnSlice; private boolean isClosed; private boolean seenEnd; protected Iterator ksIter; private KeySlice mostRecentRow; private final IPartitioner partitioner; private Token nextStartToken; private final Token endToken; private ByteBuffer nextStartKey; private boolean omitEndToken; public AbstractBufferedRowIter(IPartitioner partitioner, SliceQuery columnSlice, int pageSize, Token startToken, Token endToken, boolean omitEndToken) { this.pageSize = pageSize; this.partitioner = partitioner; this.nextStartToken = startToken; this.endToken = endToken; this.columnSlice = columnSlice; this.seenEnd = false; this.isClosed = false; this.ksIter = Iterators.emptyIterator(); this.mostRecentRow = null; this.omitEndToken = omitEndToken; } @Override public boolean hasNext() { ensureOpen(); if (!ksIter.hasNext() && !seenEnd) { try { ksIter = rebuffer().iterator(); } catch (BackendException e) { throw new RuntimeException(e); } } return ksIter.hasNext(); } @Override public StaticBuffer next() { ensureOpen(); if (!hasNext()) throw new NoSuchElementException(); mostRecentRow = ksIter.next(); Preconditions.checkNotNull(mostRecentRow); return StaticArrayBuffer.of(mostRecentRow.bufferForKey()); } @Override public void close() { closeIterator(); } @Override public void remove() { throw new UnsupportedOperationException(); } @Override public RecordIterator getEntries() { ensureOpen(); return new RecordIterator() { final Iterator columns = CassandraHelper.makeEntryIterator(mostRecentRow.getColumns(), entryGetter, columnSlice.getSliceEnd(), columnSlice.getLimit()); @Override public boolean hasNext() { ensureOpen(); return columns.hasNext(); } @Override public Entry next() { ensureOpen(); return columns.next(); } @Override public void close() { closeIterator(); } @Override public void remove() { throw new UnsupportedOperationException(); } }; } private void ensureOpen() { if (isClosed) throw new IllegalStateException("Iterator has been closed."); } private void closeIterator() { if (!isClosed) { isClosed = true; } } private List rebuffer() throws BackendException { Preconditions.checkArgument(!seenEnd); return checkFreshSlices(getNextKeySlices()); } protected List checkFreshSlices(List ks) { if (0 == ks.size()) { seenEnd = true; return Collections.emptyList(); } nextStartKey = ks.get(ks.size() - 1).bufferForKey(); nextStartToken = partitioner.getToken(nextStartKey); if (nextStartToken.equals(endToken)) { seenEnd = true; if (omitEndToken) ks.remove(ks.size() - 1); } return ks; } protected final List getNextKeySlices() throws BackendException { return getTokenSlice(nextStartToken, endToken, columnSlice, pageSize); } } private final class AllTokensIterator extends AbstractBufferedRowIter { public AllTokensIterator(IPartitioner partitioner, SliceQuery columnSlice, int pageSize) { super(partitioner, columnSlice, pageSize, partitioner.getMinimumToken(), partitioner.getMinimumToken(), false); } } private final class KeyRangeIterator extends AbstractBufferedRowIter { public KeyRangeIterator(IPartitioner partitioner, SliceQuery columnSlice, int pageSize, ByteBuffer startKey, ByteBuffer endKey) throws BackendException { super(partitioner, columnSlice, pageSize, partitioner.getToken(startKey), partitioner.getToken(endKey), true); Preconditions.checkArgument(partitioner instanceof AbstractByteOrderedPartitioner); // Get first slice with key range instead of token range. Token // ranges are start-exclusive, key ranges are start-inclusive. Both // are end-inclusive. If we don't make the call below, then we will // erroneously miss startKey. List ks = getKeySlice(startKey, endKey, columnSlice, pageSize); this.ksIter = checkFreshSlices(ks).iterator(); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy