/** * (c) Copyright 2014 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.mapreduce.framework; import java.net.InetAddress; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import com.datastax.driver.core.ResultSet; import com.datastax.driver.core.Row; import com.datastax.driver.core.Session; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Class responsible for creating subsplits. */ class CassandraSubSplitCreator { private static final Logger LOG = LoggerFactory.getLogger(CassandraSubSplitCreator.class); /** Open session. */ private final Session mSession; /** * Constructor for SubsplitCreator. * @param session open Cassandra Session. */ public CassandraSubSplitCreator(Session session) { mSession = session; // Check that this session uses the load-balancing policy that we need. Preconditions.checkArgument( session.getCluster().getConfiguration().getPolicies().getLoadBalancingPolicy() instanceof ConsistentHostOrderPolicy ); } /** * Used only for testing. */ CassandraSubSplitCreator() { mSession = null; } /** * Create a list of subsplits for this Cassandra cluster. Each subsplit contains an IP address * and a token range. * @return The subsplits. */ public List<CassandraSubSplit> createSubSplits() { // Create subsplits that initially contain a mapping from token ranges to primary hosts. Map<Long, String> tokensToMasterNodes = getTokenToMasterNodeMapping(); List<CassandraSubSplit> subsplits = createInitialSubSplits(tokensToMasterNodes); // TODO: Add replica nodes to the subsplits. return subsplits; } /** * Create an initial set of subsplits, one per vnode. * * @param tokensToMasterNodes Map from tokens to their master nodes. * @return the list of subsplits. */ List<CassandraSubSplit> createInitialSubSplits(Map<Long, String> tokensToMasterNodes) { // Go from a mapping between tokens and hosts to a mapping between token *ranges* and hosts. List<Long> sortedTokens = Lists.newArrayList(); for (Long tok : tokensToMasterNodes.keySet()) { sortedTokens.add(tok); } Collections.sort(sortedTokens); LOG.debug(String.format("Found %d total tokens", sortedTokens.size())); LOG.debug(String.format("Minimum tokens is %s", sortedTokens.get(0))); LOG.debug(String.format("Maximum tokens is %s", sortedTokens.get(sortedTokens.size() - 1))); // We need to add the global min and global max token values so that we make sure that our // subsplits cover all of the data in the cluster. sortedTokens.add(CassandraSubSplit.RING_START_TOKEN); sortedTokens.add(CassandraSubSplit.RING_END_TOKEN); Collections.sort(sortedTokens); Preconditions.checkArgument(sortedTokens.get(0) == CassandraSubSplit.RING_START_TOKEN); Preconditions.checkArgument( sortedTokens.get(sortedTokens.size() - 1) == CassandraSubSplit.RING_END_TOKEN); // Loop through all of the pairs of tokens, creating subsplits for every pair. Remember in // C* that the master node for a token gets all data between the *previous* token and the token // in question, so we assign ownership of a given subsplit to the node associated with the // second (greater) of the two tokens. List<CassandraSubSplit> subsplits = Lists.newArrayList(); for (int tokenIndex = 0; tokenIndex < sortedTokens.size() - 1; tokenIndex++) { long lowerBoundToken = sortedTokens.get(tokenIndex); long startToken = lowerBoundToken; long endToken = sortedTokens.get(tokenIndex + 1); String hostForEndToken = tokensToMasterNodes.get(endToken); if (tokenIndex == sortedTokens.size() - 2) { Preconditions.checkArgument(null == hostForEndToken); hostForEndToken = tokensToMasterNodes.get(startToken); Preconditions.checkNotNull(hostForEndToken); } Preconditions.checkNotNull(hostForEndToken); // Ownership for a given node looks like (previous token, my token], so we add 1 to the // start token, unless the start token is the first token in our entire ring. final long startTokenAdjustedForExclusive; if (tokenIndex > 0) { startTokenAdjustedForExclusive = lowerBoundToken + 1; } else { startTokenAdjustedForExclusive = lowerBoundToken; } CassandraSubSplit subsplit = CassandraSubSplit.createFromHost( startTokenAdjustedForExclusive, endToken, hostForEndToken); subsplits.add(subsplit); } return subsplits; } /** * Read metadata from our Cassandra cluster to get the mapping from tokens to master nodes. * * @return A map from tokens (stored as strings) to the master nodes (stored as IP addresses). */ private Map<Long, String> getTokenToMasterNodeMapping() { Map<Long, String> tokensToMasterNodes = Maps.newHashMap(); // Get the set of tokens for the local host. updateTokenListForLocalHost(mSession, tokensToMasterNodes); // Query the `local` and `peers` tables to get mappings from tokens to hosts. updateTokenListForPeers(mSession, tokensToMasterNodes); return tokensToMasterNodes; } /** * Update our map of tokens to master nodes by getting a list of tokens owned by the local host. * @param session An open Cassandra session. * @param tokensToHosts The map from tokens to master nodes to update. */ private void updateTokenListForLocalHost( Session session, Map<Long, String> tokensToHosts) { String queryString = "SELECT tokens FROM system.local;"; ResultSet resultSet = session.execute(queryString); List<Row> results = resultSet.all(); Preconditions.checkArgument(results.size() == 1); Set<String> tokens = results.get(0).getSet("tokens", String.class); updateTokenListForSingleNode("localhost", tokens, tokensToHosts); } /** * Update our map of tokens to master nodes by getting a list of tokens owned by peers. * @param session An open Cassandra session. * @param tokensToHosts The map from tokens to master nodes to update. */ private void updateTokenListForPeers( Session session, Map<Long, String> tokensToHosts) { String queryString = "SELECT rpc_address, tokens FROM system.peers;"; ResultSet resultSet = session.execute(queryString); for (Row row : resultSet.all()) { Set<String> tokens = row.getSet("tokens", String.class); InetAddress rpcAddress = row.getInet("rpc_address"); String hostName = rpcAddress.getHostName(); Preconditions.checkArgument(!hostName.equals("localhost")); updateTokenListForSingleNode(hostName, tokens, tokensToHosts); } } /** * Given a host and a list of tokens for which the host is the master node, update our map of * tokens to master nodes. * @param hostName The name of the host. * @param tokens A list of tokens for which the host is the master node. * @param tokensToHosts The map from tokens to master nodes to update. */ private void updateTokenListForSingleNode( String hostName, Set<String> tokens, Map<Long, String> tokensToHosts) { LOG.debug(String.format("Got %d tokens for host %s", tokens.size(), hostName)); // For every token, create an entry in the map from that token to the host. for (String token : tokens) { Long tokenAsLong = Long.parseLong(token); Preconditions.checkArgument(!tokensToHosts.containsKey(tokenAsLong)); tokensToHosts.put(tokenAsLong, hostName); } } }