org.kiji.schema.impl.cassandra.CassandraKijiPartition.java Source code

Java tutorial

Introduction

Here is the source code for org.kiji.schema.impl.cassandra.CassandraKijiPartition.java

Source

/**
 * (c) Copyright 2015 WibiData, Inc.
 *
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * 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 org.kiji.schema.impl.cassandra;

import static com.datastax.driver.core.querybuilder.QueryBuilder.select;

import java.net.InetAddress;
import java.util.Collection;
import java.util.Map;
import java.util.Map.Entry;
import java.util.SortedMap;

import com.datastax.driver.core.Host;
import com.datastax.driver.core.ResultSet;
import com.datastax.driver.core.ResultSetFuture;
import com.datastax.driver.core.Row;
import com.datastax.driver.core.Session;
import com.google.common.base.Objects;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterators;
import com.google.common.collect.Maps;
import com.google.common.collect.PeekingIterator;
import com.google.common.collect.Range;
import com.google.common.collect.RangeSet;
import com.google.common.collect.TreeRangeSet;

import org.kiji.annotations.ApiAudience;
import org.kiji.annotations.ApiStability;
import org.kiji.schema.InternalKijiError;
import org.kiji.schema.KijiPartition;

/**
 * A Cassandra Kiji Partition.  Corresponds to the token range of a Cassandra VNode.
 */
@ApiAudience.Framework
@ApiStability.Experimental
public class CassandraKijiPartition implements KijiPartition {

    /** Host of token range. */
    private final InetAddress mHost;

    /** The token range. */
    private final Range<Long> mTokenRange;

    /**
     * Construct a new token range.
     *
     * @param host The partition host.
     * @param tokenRange The token range.
     */
    public CassandraKijiPartition(final InetAddress host, final Range<Long> tokenRange) {
        mHost = host;
        mTokenRange = tokenRange;
    }

    /**
     * Get the host of this partition.
     *
     * @return The host of this partition.
     */
    public InetAddress getHost() {
        return mHost;
    }

    /**
     * The token range of this partition.
     *
     * @return The token range for this partition.
     */
    public Range<Long> getTokenRange() {
        return mTokenRange;
    }

    /** {@inheritDoc} */
    @Override
    public String toString() {
        return Objects.toStringHelper(this).add("host", mHost).add("token-range", mTokenRange).toString();
    }

    /**
     * Get the Cassandra Kiji Partitions for the given cluster.
     *
     * @param session An open connection to the cluster.
     * @return The collection of Kiji partitions.
     */
    public static Collection<CassandraKijiPartition> getPartitions(Session session) {
        final SortedMap<Long, InetAddress> startTokens = getStartTokens(session);
        final Map<Range<Long>, InetAddress> tokenRanges = getTokenRanges(startTokens);

        final ImmutableList.Builder<CassandraKijiPartition> partitions = ImmutableList.builder();

        for (Map.Entry<Range<Long>, InetAddress> tokenRange : tokenRanges.entrySet()) {
            partitions.add(new CassandraKijiPartition(tokenRange.getValue(), tokenRange.getKey()));
        }

        return partitions.build();
    }

    /**
     * Retrieve the set of (start-token, host) pairs of a cluster sorted by start token.
     *
     * Package private for testing.
     *
     * @param session A connection to the cluster.
     * @return The set of (start-token, host) pairs.
     */
    static SortedMap<Long, InetAddress> getStartTokens(Session session) {
        // TODO(WDSCHEMA-383): Replace all of this logic with the Cassandra driver api

        // Cassandra lets us query for the coordinator-local tokens as well as the coordinator peer
        // tokens, but they are split up into different tables. Accordingly, we have to make sure that
        // when the two queries are executed, the coordinator nodes are consistent. In a typical
        // Cassandra installation with multiple nodes, the Cassandra driver will round robin between
        // nodes, so we counteract this by requesting the peers from two different nodes and merging the
        // results. For more information on Cassandra system tables:
        //    https://www.datastax.com/documentation/cql/3.0/cql/cql_using/use_query_system_c.html

        final ResultSetFuture localTokensFuture = session.executeAsync(select("tokens").from("system", "local"));
        final ResultSetFuture peerTokensFuture1 = session
                .executeAsync(select("rpc_address", "tokens").from("system", "peers"));
        final ResultSetFuture peerTokensFuture2 = session
                .executeAsync(select("rpc_address", "tokens").from("system", "peers"));

        final ResultSet localTokens = localTokensFuture.getUninterruptibly();
        final ResultSet peerTokens1 = peerTokensFuture1.getUninterruptibly();
        final ResultSet peerTokens2 = peerTokensFuture2.getUninterruptibly();

        final Host localHost = localTokens.getExecutionInfo().getQueriedHost();
        final Host peerHost1 = peerTokens1.getExecutionInfo().getQueriedHost();
        final Host peerHost2 = peerTokens2.getExecutionInfo().getQueriedHost();

        // If this assert ever fails in practice, we may need to implement auto-retry.
        if (!(!peerHost1.equals(peerHost2) // consistent because peer1/peer2 sets will be comprehensive
                || localHost.equals(peerHost1) // consistent because local/peer1 sets will be comprehensive
                || localHost.equals(peerHost2) // consistent because local/peer2 sets will be comprehensive
        )) {
            throw new InternalKijiError(
                    "Coordinator nodes must be consistent across local and peer token range queries."
                            + String.format(" local host: %s, peer host 1: %s, peer host 2: %s.", localHost,
                                    peerHost1, peerHost2)
                            + " Please retry.");
        }

        final InetAddress coordinator = localTokens.getExecutionInfo().getQueriedHost().getSocketAddress()
                .getAddress();

        SortedMap<Long, InetAddress> tokens = Maps.newTreeMap();

        for (Row row : localTokens.all()) {
            for (String token : row.getSet("tokens", String.class)) {
                tokens.put(Long.parseLong(token), coordinator);
            }
        }

        for (Row row : peerTokens1.all()) {
            final InetAddress peer = row.getInet("rpc_address");
            for (String token : row.getSet("tokens", String.class)) {
                tokens.put(Long.parseLong(token), peer);
            }
        }

        for (Row row : peerTokens2.all()) {
            final InetAddress peer = row.getInet("rpc_address");
            for (String token : row.getSet("tokens", String.class)) {
                tokens.put(Long.parseLong(token), peer);
            }
        }

        return tokens;
    }

    /**
     * Convert a set of (start-token, host) pairs into a set of (token-range, host) pairs.
     *
     * Package private for testing.
     *
     * @param startTokens The set of start tokens with hosts.
     * @return The token corresponding token ranges.
     */
    static Map<Range<Long>, InetAddress> getTokenRanges(final SortedMap<Long, InetAddress> startTokens) {

        ImmutableMap.Builder<Range<Long>, InetAddress> tokenRangesBldr = ImmutableMap.builder();

        final PeekingIterator<Entry<Long, InetAddress>> startTokensItr = Iterators
                .peekingIterator(startTokens.entrySet().iterator());

        // Add a range for [-, firstStartToken) owned by the final key (the wrap-around range).
        // For more information on Casandra VNode token ranges:
        //    http://www.datastax.com/dev/blog/virtual-nodes-in-cassandra-1-2
        tokenRangesBldr.put(Range.lessThan(startTokens.firstKey()), startTokens.get(startTokens.lastKey()));

        while (startTokensItr.hasNext()) {
            Entry<Long, InetAddress> startToken = startTokensItr.next();
            if (!startTokensItr.hasNext()) {
                // The final start token
                // Add a range for [lastStartToken, )
                tokenRangesBldr.put(Range.atLeast(startToken.getKey()), startToken.getValue());
            } else {
                // Add a range for [thisStartToken, nextStartToken)
                tokenRangesBldr.put(Range.closedOpen(startToken.getKey(), startTokensItr.peek().getKey()),
                        startToken.getValue());
            }
        }

        final Map<Range<Long>, InetAddress> tokenRanges = tokenRangesBldr.build();

        // Check that the returned ranges are coherent; most importantly that all possible tokens fall
        // within the returned range set.

        if (startTokens.size() + 1 != tokenRanges.size()) {
            throw new InternalKijiError(
                    String.format("Unexpected number of token ranges. start-tokens: %s, token-ranges: %s.",
                            startTokens.size(), tokenRanges.size()));
        }

        final RangeSet<Long> ranges = TreeRangeSet.create();
        for (Range<Long> tokenRange : tokenRanges.keySet()) {
            ranges.add(tokenRange);
        }

        if (!ranges.encloses(Range.closed(Long.MIN_VALUE, Long.MAX_VALUE))) {
            throw new InternalKijiError("Token range does not include all possible tokens.");
        }

        return tokenRanges;
    }
}