/* * @(#) OdklDomainPartitioner.java * Created 21.06.2011 by oleg * (C) ONE, SIA */ package org.apache.cassandra.dht; import java.net.InetAddress; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Random; import org.apache.cassandra.config.ConfigurationException; import org.apache.cassandra.config.DatabaseDescriptor; import org.apache.cassandra.db.DecoratedKey; import org.apache.cassandra.locator.AbstractReplicationStrategy; import org.apache.cassandra.locator.TokenMetadata; import org.apache.cassandra.service.StorageService; import org.apache.cassandra.utils.FBUtilities; import org.apache.log4j.Logger; /** * This partitioner expects to find keys as long numbers in form of hex strings, in which last 2 chars represent odnoklassniki domain byte. * Say, you have key 168efd9e, so partitioning will performed by last byte of this string, which is 0x9e. * * As an internal representation it transforms key to token by placing last 2 chars first in {@link StringToken} * * So token of id=168efd9e will be 9e168eafd. * * @author Oleg Anastasyev<oa@hq.one.lv> * */ public class OdklDomainPartitioner extends OrderPreservingPartitioner { public static final StringToken MINIMUM = new StringToken("00"); private static final Logger logger = Logger.getLogger(OdklDomainPartitioner.class); /* (non-Javadoc) * @see org.apache.cassandra.dht.OrderPreservingPartitioner#getMinimumToken() */ @Override public StringToken getMinimumToken() { return MINIMUM; } private boolean isHexDigit(char c) { return ( c>='0' && c<='9' ) || (c>='a' && c<='f'); } protected StringToken toStringToken(String key) { int klen = key.length(); if (klen<2) { assert klen>0 : "Size of key is minimum single digit to partition odnoklassniki style"; char c = fastToLower( key.charAt(0) ); if (isHexDigit(c)) return new StringToken(new String(new char[] { '0',c })); if (logger.isDebugEnabled()) logger.debug("Last 2 chars of key must be hex digits, but in "+key+" they're not. This is ok for system table. reverting to OrderedPartitioner"); return new StringToken(String.valueOf(c)); } if (klen==2) { char c0 = fastToLower( key.charAt(0) ); char c1 = fastToLower( key.charAt(1) ); if (isHexDigit(c0) && isHexDigit(c1)) { return new StringToken(new String( new char[] { c0,c1 })); } } char[] ctoken = new char[klen]; ctoken[0] = fastToLower(key.charAt(klen-2)); ctoken[1] = fastToLower(key.charAt(klen-1)); if ( !isHexDigit(ctoken[0]) || !isHexDigit(ctoken[1]) ) { if (logger.isDebugEnabled()) logger.debug("Last 2 chars of key must be hex digits, but in "+key+" they're not. This is ok for system table. reverting to OrderedPartitioner"); return new StringToken(key.toLowerCase()); } fastToLower(key,0,klen-2,ctoken,2); return new StringToken(new String(ctoken)); } public DecoratedKey<StringToken> decorateKey(String key) { return new DecoratedKey<StringToken>(toStringToken(key), key); } public DecoratedKey<StringToken> convertFromDiskFormat(String key) { return new DecoratedKey<StringToken>(toStringToken(key), key); } /* (non-Javadoc) * @see org.apache.cassandra.dht.OrderPreservingPartitioner#getRandomToken() */ @Override public StringToken getRandomToken() { byte[] rnd = new byte[1]; new Random().nextBytes(rnd); return new StringToken(FBUtilities.bytesToHex(rnd)); } /* (non-Javadoc) * @see org.apache.cassandra.dht.OrderPreservingPartitioner#getToken(java.lang.String) */ @Override public StringToken getToken(String key) { return toStringToken(key); } public StringToken toStringToken(int domain) { StringBuilder sb = new StringBuilder(2); if (domain<0x10) sb.append('0'); return new StringToken( sb.append(Integer.toHexString(domain)).toString() ); } public StringToken toStringToken(int domain, String tail) { if (tail.length()<=2) return toStringToken(domain); StringBuilder sb = new StringBuilder(tail.length()); if (domain<0x10) sb.append('0'); return new StringToken( sb.append(Integer.toHexString(domain)).append(tail, 2,tail.length()).toString() ); } /** * Validates, that token is hex digit 0-255, lower case letters only * * @param initialToken * @throws ConfigurationException if something is wrong */ @Override public void validateToken(Token nodeToken) throws ConfigurationException { String initialToken = nodeToken.toString(); if (initialToken==null) throw new ConfigurationException("Initial node token must be specified explicitly"); if (initialToken.length()!=2) throw new ConfigurationException("Node token must be 2 char hex digit : "+initialToken); try { int token = Integer.parseInt(initialToken, 16); if (token<0 || token>255) throw new ConfigurationException("Node token must be hex digit 0-255 : "+initialToken); if (!initialToken.toLowerCase().equals(initialToken)) throw new ConfigurationException("Node token must be hex digit. Only lower case letters allowed : "+initialToken); } catch (NumberFormatException e) { throw new ConfigurationException("Node token must be hex digit : "+initialToken); } } /** * Ownership calc for odkl partitioner is much simpler. There are only 256 distinct domains, so ownership is just domaincount/256 % */ @Override public Map<Token, Float> describeOwnership(List<Token> sortedTokens) { TokenMetadata tm = StorageService.instance.getTokenMetadata(); Map<InetAddress,Integer> rangesPerEndpoint = new HashMap<InetAddress,Integer>(); float totalCount = 0; for (String table : DatabaseDescriptor.getNonSystemTables() ) { AbstractReplicationStrategy strategy = StorageService.instance.getReplicationStrategy(table); for (int domain=0;domain<256;domain++) { ArrayList<InetAddress> endpoints = strategy.getNaturalEndpoints(toStringToken(domain, "1"), table); for (InetAddress p : endpoints) { Integer count = rangesPerEndpoint.get(p); if (count==null) count=1; else count+=1; rangesPerEndpoint.put(p,count); totalCount ++; } } } Map<Token, Float> alltokens = new HashMap<Token, Float>(); for ( Entry<InetAddress, Integer> en : rangesPerEndpoint.entrySet() ) { if (tm.isMember(en.getKey())) { alltokens.put( tm.getToken(en.getKey()), en.getValue() / totalCount ); } } return alltokens; } /* * Trying to make faster tolowercase assuming already lowered case in key on most cases and * ascii chars more preferable there. * If these assumptions fail - reverts to String.toLowerCase */ private void fastToLower(String s,int sstart,int send,char[] chars,int cstart) { while ( sstart<send) { chars[cstart++]= fastToLower(s.charAt(sstart++)); } } private char fastToLower(char c) { if (c <= 0x7f) return c<'A' || c>'Z' ? c : (char) (c-'A'+'a'); return Character.toLowerCase(c); } /* public static void main(String[] args) { OdklDomainPartitioner p = new OdklDomainPartitioner(); StringToken k = p.toStringToken("abcd"); System.out.println(k); k = p.toStringToken("1234aB-123Cd"); System.out.println(k); k = p.toStringToken("ебанаab"); System.out.println(k); k = p.toStringToken("ЕБАНАab"); System.out.println(k); k = p.toStringToken("B"); System.out.println(k); k = p.toStringToken("AB"); System.out.println(k); } */ }