Package org.voltdb

Source Code of org.voltdb.TestTheHashinator

/* This file is part of VoltDB.
* Copyright (C) 2008-2014 VoltDB Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
* OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/

package org.voltdb;

import static junit.framework.Assert.assertNull;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.TreeMap;

import org.junit.Before;
import org.junit.Test;
import org.voltcore.utils.InstanceId;
import org.voltcore.utils.Pair;
import org.voltdb.TheHashinator.HashinatorConfig;
import org.voltdb.TheHashinator.HashinatorType;
import org.voltdb.jni.ExecutionEngine;
import org.voltdb.jni.ExecutionEngineJNI;
import org.voltdb.sysprocs.saverestore.HashinatorSnapshotData;

import com.google_voltpatches.common.collect.HashMultimap;
import com.google_voltpatches.common.collect.ImmutableSortedMap;
import com.google_voltpatches.common.collect.Maps;
import com.google_voltpatches.common.collect.Multimaps;
import com.google_voltpatches.common.collect.SetMultimap;
import com.google_voltpatches.common.collect.SortedMapDifference;

/**
* This test verifies that the Java Hashinator behaves
* identically as the C++ Hashinator.
*
*/
public class TestTheHashinator {
    Random r = new Random();

    @Before
    public void setUp() {
        ElasticHashinator.DEFAULT_TOTAL_TOKENS = 1024;
        EELibraryLoader.loadExecutionEngineLibrary(true);
        VoltDB.instance().readBuildInfo("Test");
    }

    private final HashinatorType hashinatorType = TheHashinator.getConfiguredHashinatorType();
    private final int tokensPerPartition = 6;

    private static Map<Integer, Integer> deserializeElasticConfig(byte[] config) {
        Map<Integer, Integer> tokens = new HashMap<Integer, Integer>();
        ByteBuffer buf = ByteBuffer.wrap(config);
        int count = buf.getInt();

        for (int i = 0; i < count; i++) {
            tokens.put(buf.getInt(), buf.getInt());
        }

        return tokens;
    }

    /** make sure that every range hashes to partition */
    private static void checkRangeBoundaries(ElasticHashinator hashinator,
                                             int partition,
                                             Map<Integer, Integer> range)
    {
        for (Map.Entry<Integer, Integer> entry : range.entrySet()) {
            int start = entry.getKey();
            int end = entry.getValue();
            assertEquals(partition, hashinator.partitionForToken(start));
            if (end != Long.MIN_VALUE) {
                assertEquals(partition, hashinator.partitionForToken(end - 1));
            } else {
                assertEquals(partition, hashinator.partitionForToken(Integer.MAX_VALUE));
            }
        }
    }

    /** make sure that every token for partition appears in the range map */
    private static void checkTokensInRanges(ElasticHashinator hashinator,
                                            int partition,
                                            Map<Integer, Integer> range)
    {
        byte[] config = hashinator.pGetCurrentConfig().configBytes;
        Map<Integer, Integer> tokens = deserializeElasticConfig(config);
        for (Map.Entry<Integer, Integer> entry : tokens.entrySet()) {
            int token = entry.getKey();
            int pid = entry.getValue();

            if (pid == partition) {
                boolean foundRange = false;
                for (Map.Entry<Integer, Integer> rangeEntry : range.entrySet()) {
                    int start = rangeEntry.getKey();
                    int end = rangeEntry.getValue();
                    if (start <= token && token < end) {
                        foundRange = true;
                        break;
                    } else if (end < start && (start <= token || token < end)) {
                        // Boundary case, [start, end) wraps around the origin
                        foundRange = true;
                        break;
                    }
                }

                assertTrue(foundRange);
            }
        }
    }

    private static Map<Integer, Integer> getRangesAndCheck(int partitionCount, int partitionToCheck)
    {
        ElasticHashinator hashinator =
            new ElasticHashinator(ElasticHashinator.getConfigureBytes(partitionCount,
                                                                      ElasticHashinator.DEFAULT_TOTAL_TOKENS),
                                                                      false);
        Map<Integer, Integer> range1 = hashinator.pGetRanges(partitionToCheck);
        assertTrue(ElasticHashinator.DEFAULT_TOTAL_TOKENS / partitionCount == range1.size() ||
                   1 + ElasticHashinator.DEFAULT_TOTAL_TOKENS / partitionCount == range1.size());

        // make sure that the returned map is sorted
        long previous = Integer.MIN_VALUE;
        for (long k : range1.keySet()) {
            assertTrue(k >= previous);
            previous = k;
        }

        checkRangeBoundaries(hashinator, partitionToCheck, range1);
        checkTokensInRanges(hashinator, partitionToCheck, range1);

        // non-existing partition should have an empty range
        assertTrue(hashinator.pGetRanges(partitionCount + 1).isEmpty());

        return range1;
    }

    /**
     * Compare the tokens of the covered partitions before and after adding new partitions to
     * the hashinator. The tokens should match.
     * @param beforePartitionCount    Partition count before adding new partitions
     * @param afterPartitionCount     Partition count after adding new partitions
     */
    private static void checkRangesAfterExpansion(int beforePartitionCount, int afterPartitionCount) {
        for (int i = 0; i < beforePartitionCount; i++) {
            Map<Integer, Integer> oldrange = getRangesAndCheck(beforePartitionCount, i);
            Map<Integer, Integer> newrange = getRangesAndCheck(afterPartitionCount, i);

            // Only compare the begin tokens, end tokens are determined by the successors
            assertTrue(oldrange.keySet().containsAll(newrange.keySet()));
            assertTrue(oldrange.keySet().size() > newrange.keySet().size());
            assertTrue(newrange.keySet().size() > 0);
            assertTrue(oldrange.size() == ElasticHashinator.DEFAULT_TOTAL_TOKENS / beforePartitionCount ||
                       oldrange.size() == 1 + ElasticHashinator.DEFAULT_TOTAL_TOKENS / beforePartitionCount);
            assertTrue(newrange.size() == ElasticHashinator.DEFAULT_TOTAL_TOKENS / afterPartitionCount ||
                       newrange.size() == 1 + ElasticHashinator.DEFAULT_TOTAL_TOKENS / afterPartitionCount);
        }
    }


    private static SetMultimap<Integer, Integer> invertTokenMap(Map<Integer, Integer> m) {
        return Multimaps.invertFrom(Multimaps.forMap(m), HashMultimap.<Integer, Integer>create());
    }

    @Test
    public void testGetPartitionKeys() throws Exception {
        int partitionCount = 50;
        Set<Integer> allPartitions = new HashSet<Integer>();
        for (int ii = 0; ii < 50; ii++) {
            allPartitions.add(ii);
        }
        VoltType types[] = new VoltType[] { VoltType.INTEGER, VoltType.STRING, VoltType.VARBINARY };

        final byte configBytes[] = TheHashinator.getConfigureBytes(partitionCount);
        TheHashinator.initialize(TheHashinator.getConfiguredHashinatorClass(), configBytes);
        for (VoltType type : types) {
            Set<Integer> partitionsReached = new HashSet<Integer>(allPartitions);
            assertNull(TheHashinator.getPartitionKeys(VoltType.BIGINT));
            VoltTable vt = TheHashinator.getPartitionKeys(type);
            assertNotNull(vt);
            assertEquals(partitionCount, vt.getRowCount());

            while (vt.advanceRow()) {
                int expectedPartition = (int)vt.getLong(0);
                Object key = vt.get(1, type);
                assertEquals(expectedPartition, TheHashinator.getPartitionForParameter(type.getValue(), key));
                partitionsReached.remove(expectedPartition);
            }
            assertTrue(partitionsReached.isEmpty());
        }

    }
    /*
     * This test validates that not all values hash to 0. Most of the other
     * tests will pass even if everything hashes to a single partition.
     */
    @Test
    public void testExpectNonZeroHash() throws Exception {
        int partitionCount = 3;
        final byte configBytes[] = TheHashinator.getConfigureBytes(partitionCount);
        TheHashinator.initialize(TheHashinator.getConfiguredHashinatorClass(), configBytes);
        HashinatorConfig config = TheHashinator.getCurrentConfig();
        ExecutionEngine ee =
                new ExecutionEngineJNI(
                        1,
                        1,
                        0,
                        0,
                        "",
                        100,
                        config);

        long valueToHash = hashinatorType == HashinatorType.ELASTIC ? 39: 2;

        int eehash = ee.hashinate(valueToHash, config);
        int javahash = TheHashinator.getPartitionForParameter(VoltType.typeFromObject(valueToHash).getValue(),
                valueToHash);
        if (eehash != javahash) {
            System.out.printf("Mismatched hash of (%s) %d with %d partitions => EE: %d, Java: %d\n",
                    VoltType.typeFromObject(valueToHash).toSQLString(),
                    valueToHash, partitionCount, eehash, javahash);
        }
        assertEquals(eehash, javahash);
        assertNotSame(0, eehash);
        assertTrue(eehash < partitionCount);
        assertTrue(eehash >= 0);

        try { ee.release(); } catch (Exception e) {}
    }

    @Test
    public void testSameLongHash1() throws Exception {
        int partitionCount = 2;
        TheHashinator.initialize(TheHashinator.getConfiguredHashinatorClass(), TheHashinator.getConfigureBytes(partitionCount));
        HashinatorConfig hashinatorConfig = TheHashinator.getCurrentConfig();
        ExecutionEngine ee =
                new ExecutionEngineJNI(
                        1,
                        1,
                        0,
                        0,
                        "",
                        100,
                        hashinatorConfig);


        long valueToHash = 0;
        int eehash = ee.hashinate(valueToHash, hashinatorConfig);
        int javahash = TheHashinator.getPartitionForParameter(VoltType.typeFromObject(valueToHash).getValue(),
                valueToHash);
        if (eehash != javahash) {
            System.out.printf("Mismatched hash of (%s) %d with %d partitions => EE: %d, Java: %d\n",
                    VoltType.typeFromObject(valueToHash).toSQLString(),
                    valueToHash, partitionCount, eehash, javahash);
        }
        assertEquals(eehash, javahash);
        assertTrue(eehash < partitionCount);
        assertTrue(eehash >= 0);

        valueToHash = 1;
        eehash = ee.hashinate(valueToHash, hashinatorConfig);
        javahash = TheHashinator.getPartitionForParameter(VoltType.typeFromObject(valueToHash).getValue(),
                valueToHash);
        if (eehash != javahash) {
            System.out.printf("Mismatched hash of (%s) %d with %d partitions => EE: %d, Java: %d\n",
                    VoltType.typeFromObject(valueToHash).toSQLString(),
                    valueToHash, partitionCount, eehash, javahash);
        }
        assertEquals(eehash, javahash);
        assertTrue(eehash < partitionCount);
        assertTrue(eehash >= 0);

        valueToHash = 2;
        eehash = ee.hashinate(valueToHash, hashinatorConfig);
        javahash = TheHashinator.getPartitionForParameter(VoltType.typeFromObject(valueToHash).getValue(),
                valueToHash);
        if (eehash != javahash) {
            System.out.printf("Mismatched hash of (%s) %d with %d partitions => EE: %d, Java: %d\n",
                    VoltType.typeFromObject(valueToHash).toSQLString(),
                    valueToHash, partitionCount, eehash, javahash);
        }
        assertEquals(eehash, javahash);
        assertTrue(eehash < partitionCount);
        assertTrue(eehash >= 0);

        valueToHash = 3;
        eehash = ee.hashinate(valueToHash, hashinatorConfig);
        javahash = TheHashinator.getPartitionForParameter(VoltType.typeFromObject(valueToHash).getValue(),
                valueToHash);
        if (eehash != javahash) {
            System.out.printf("Mismatched hash of (%s) %d with %d partitions => EE: %d, Java: %d\n",
                    VoltType.typeFromObject(valueToHash).toSQLString(),
                    valueToHash, partitionCount, eehash, javahash);
        }
        assertEquals(eehash, javahash);
        assertTrue(eehash < partitionCount);
        assertTrue(eehash >= 0);

        try { ee.release(); } catch (Exception e) {}
    }

    @Test
    public void testSizeChanges() {
        // try with lots of partition counts
        for (int partitionCount = 1; partitionCount <= 11; partitionCount++) {
            TheHashinator.initialize(TheHashinator.getConfiguredHashinatorClass(), TheHashinator.getConfigureBytes(partitionCount));
            HashinatorConfig hashinatorConfig = TheHashinator.getCurrentConfig();
            ExecutionEngine ee =
                    new ExecutionEngineJNI(
                            1,
                            1,
                            0,
                            0,
                            "",
                            100,
                            hashinatorConfig);

            // use a short value hashed as a long type
            for (short valueToHash = -7; valueToHash <= 7; valueToHash++) {
                int eehash = ee.hashinate(valueToHash, hashinatorConfig);
                int javahash = TheHashinator.getPartitionForParameter(VoltType.BIGINT.getValue(), valueToHash);
                if (eehash != javahash) {
                    System.out.printf("Mismatched hash of (%s) %d with %d partitions => EE: %d, Java: %d\n",
                            VoltType.typeFromObject(valueToHash).toSQLString(),
                            valueToHash, partitionCount, eehash, javahash);
                }
                assertEquals(eehash, javahash);
                assertTrue(eehash < partitionCount);
                assertTrue(eehash >= 0);
            }

            // use a long value hashed as a short type
            for (long valueToHash = -7; valueToHash <= 7; valueToHash++) {
                int eehash = ee.hashinate(valueToHash, hashinatorConfig);
                int javahash = TheHashinator.getPartitionForParameter(VoltType.SMALLINT.getValue(), valueToHash);
                if (eehash != javahash) {
                    System.out.printf("Mismatched hash of (%s) %d with %d partitions => EE: %d, Java: %d\n",
                            VoltType.typeFromObject(valueToHash).toSQLString(),
                            valueToHash, partitionCount, eehash, javahash);
                }
                assertEquals(eehash, javahash);
                assertTrue(eehash < partitionCount);
                assertTrue(eehash >= 0);
            }

            try { ee.release(); } catch (Exception e) {}
        }
    }

    @Test
    public void testEdgeCases() throws Exception {
        byte configBytes[] = TheHashinator.getConfigureBytes(1);
        ExecutionEngine ee =
                new ExecutionEngineJNI(
                        1,
                        1,
                        0,
                        0,
                        "",
                        100,
                        new HashinatorConfig(hashinatorType, configBytes, 0, 0));

        /**
         *  Run with 100k of random values and make sure C++ and Java hash to
         *  the same value.
         */
        for (int i = 0; i < 500; i++) {
            int partitionCount = r.nextInt(1000) + 1;
            long[] values = new long[] {
                    Long.MIN_VALUE, Long.MAX_VALUE, Long.MAX_VALUE - 1, Long.MIN_VALUE + 1
            };
            configBytes = TheHashinator.getConfigureBytes(partitionCount);
            TheHashinator.initialize(TheHashinator.getConfiguredHashinatorClass(), configBytes);
            for (long valueToHash : values) {
                int eehash = ee.hashinate(valueToHash, TheHashinator.getCurrentConfig());
                int javahash = TheHashinator.getPartitionForParameter(VoltType.typeFromObject(valueToHash).getValue(),
                        valueToHash);
                if (eehash != javahash) {
                    System.out.printf("Mismatched hash of (%s) %d with %d partitions => EE: %d, Java: %d\n",
                            VoltType.typeFromObject(valueToHash).toSQLString(),
                            valueToHash, partitionCount, eehash, javahash);
                }
                assertEquals(eehash, javahash);
                assertTrue(eehash < partitionCount);
                assertTrue(eehash >= 0);
            }
        }

        try { ee.release(); } catch (Exception e) {}
    }

    @Test
    public void testSameLongHash() throws Exception {
        byte configBytes[] = TheHashinator.getConfigureBytes(1);
        ExecutionEngine ee = new ExecutionEngineJNI(1, 1, 0, 0, "", 100, new HashinatorConfig(hashinatorType, configBytes, 0, 0));

        /**
         *  Run with 10k of random values and make sure C++ and Java hash to
         *  the same value.
         */
        for (int i = 0; i < 1500; i++) {
            final int partitionCount = r.nextInt(1000) + 1;
            configBytes = TheHashinator.getConfigureBytes(partitionCount);
            TheHashinator.initialize(TheHashinator.getConfiguredHashinatorClass(), configBytes);
            // this will produce negative values, which is desired here.
            final long valueToHash = r.nextLong();
            final int javahash = TheHashinator.getPartitionForParameter(VoltType.typeFromObject(valueToHash).getValue(),
                                                                        valueToHash);
            final int eehash = ee.hashinate(valueToHash, TheHashinator.getCurrentConfig());
            if (eehash != javahash) {
                System.out.printf("Mismatched hash of (%s) %d with %d partitions => EE: %d, Java: %d\n",
                        VoltType.typeFromObject(valueToHash).toSQLString(),
                        valueToHash, partitionCount, eehash, javahash);
            }
            assertEquals(eehash, javahash);
            assertTrue(eehash < partitionCount);
            assertTrue(eehash > -1);
        }

        try { ee.release(); } catch (Exception e) {}
    }

    @Test
    public void testSameStringHash() throws Exception {
        byte configBytes[] = TheHashinator.getConfigureBytes(1);
        ExecutionEngine ee =
                new ExecutionEngineJNI(
                        1,
                        1,
                        0,
                        0,
                        "",
                        100,
                        new HashinatorConfig(hashinatorType, configBytes, 0, 0));

        for (int i = 0; i < 1500; i++) {
            int partitionCount = r.nextInt(1000) + 1;
            configBytes = TheHashinator.getConfigureBytes(partitionCount);
            String valueToHash = Long.toString(r.nextLong());
            TheHashinator.initialize(TheHashinator.getConfiguredHashinatorClass(), configBytes);

            int eehash = ee.hashinate(valueToHash, TheHashinator.getCurrentConfig());
            int javahash = TheHashinator.getPartitionForParameter(VoltType.typeFromObject(valueToHash).getValue(),
                                                                  valueToHash);
            if (eehash != javahash) {
                System.out.printf("Mismatched hash of (%s) %d with %d partitions => EE: %d, Java: %d\n",
                        VoltType.typeFromObject(valueToHash).toSQLString(),
                        valueToHash, partitionCount, eehash, javahash);
                partitionCount++;
            }
            assertEquals(eehash, javahash);
            assertTrue(eehash < partitionCount);
            assertTrue(eehash >= 0);
        }

        try { ee.release(); } catch (Exception e) {}
    }

    // We coerce string representations of numeric types into that numeric type.
    // Verify this works.  We also, for legacy, coerce byte arrays of numeric types
    // into that type.  Verify this at the same time.
    @Test
    public void testNumberCoercionHash() throws Exception {
        System.out.println("=======================");
        System.out.println("NUMBER COERCION");
        for (int i = 0; i < 1500; i++) {
            int partitionCount = r.nextInt(1000) + 1;
            byte[] configBytes = TheHashinator.getConfigureBytes(partitionCount);
            long longToHash = r.nextLong();
            String stringToHash = Long.toString(longToHash);
            byte[] bufToHash;
            ByteBuffer buf = ByteBuffer.allocate(8);
            buf.order(ByteOrder.LITTLE_ENDIAN);
            buf.putLong(longToHash);
            bufToHash = buf.array();
            TheHashinator.initialize(TheHashinator.getConfiguredHashinatorClass(), configBytes);
            int longHash =
                TheHashinator.getPartitionForParameter(VoltType.BIGINT.getValue(),
                    longToHash);
            int stringHash =
                TheHashinator.getPartitionForParameter(VoltType.BIGINT.getValue(),
                    stringToHash);
            int bufHash =
                TheHashinator.getPartitionForParameter(VoltType.BIGINT.getValue(),
                    bufToHash);
            assertEquals(longHash, stringHash);
            assertEquals(stringHash, bufHash);
            assertTrue(longHash < partitionCount);
            assertTrue(longHash >= 0);
        }
    }

    @Test
    public void testNulls() throws Exception {
        ExecutionEngine ee =
                new ExecutionEngineJNI(
                        1,
                        1,
                        0,
                        0,
                        "",
                        100,
                        new HashinatorConfig(hashinatorType, TheHashinator.getConfigureBytes(2), 0, 0));
        final byte configBytes[] = TheHashinator.getConfigureBytes(2);
        TheHashinator.initialize(TheHashinator.getConfiguredHashinatorClass(), configBytes);
        int jHash =
            TheHashinator.getPartitionForParameter(VoltType.TINYINT.getValue(), new Byte(VoltType.NULL_TINYINT));
        int cHash = ee.hashinate(VoltType.NULL_TINYINT, TheHashinator.getCurrentConfig());
        assertEquals(0, jHash);
        assertEquals(jHash, cHash);
        System.out.println("jhash " + jHash + " chash " + cHash);

        jHash = TheHashinator.getPartitionForParameter(VoltType.SMALLINT.getValue(),
                new Short(VoltType.NULL_SMALLINT));
        cHash = ee.hashinate(VoltType.NULL_SMALLINT, TheHashinator.getCurrentConfig());
        assertEquals(0, jHash);
        assertEquals(jHash, cHash);
        System.out.println("jhash " + jHash + " chash " + cHash);

        jHash = TheHashinator.getPartitionForParameter(VoltType.INTEGER.getValue(),
                new Integer(VoltType.NULL_INTEGER));
        cHash = ee.hashinate(
                VoltType.NULL_INTEGER,
                TheHashinator.getCurrentConfig());
        assertEquals(0, jHash);
        assertEquals(jHash, cHash);
        System.out.println("jhash " + jHash + " chash " + cHash);

        jHash = TheHashinator.getPartitionForParameter(VoltType.BIGINT.getValue(), new Long(VoltType.NULL_BIGINT));
        cHash = ee.hashinate(
                VoltType.NULL_BIGINT,
                TheHashinator.getCurrentConfig());
        assertEquals(0, jHash);
        assertEquals(jHash, cHash);
        System.out.println("jhash " + jHash + " chash " + cHash);

        jHash = TheHashinator.getPartitionForParameter(VoltType.STRING.getValue(),
                VoltType.NULL_STRING_OR_VARBINARY);
        cHash = ee.hashinate(
                VoltType.NULL_STRING_OR_VARBINARY,
                TheHashinator.getCurrentConfig());
        assertEquals(0, jHash);
        assertEquals(jHash, cHash);
        System.out.println("jhash " + jHash + " chash " + cHash);

        jHash = TheHashinator.getPartitionForParameter(VoltType.VARBINARY.getValue(), null);
        cHash = ee.hashinate(
                null,
                TheHashinator.getCurrentConfig());
        assertEquals(0, jHash);
        assertEquals(jHash, cHash);
        System.out.println("jhash " + jHash + " chash " + cHash);

        try { ee.release(); } catch (Exception e) {}
    }

    @Test
    public void testSameBytesHash() throws Exception {
        ExecutionEngine ee =
                new ExecutionEngineJNI(
                        1,
                        1,
                        0,
                        0,
                        "",
                        100,
                        new HashinatorConfig(hashinatorType, TheHashinator.getConfigureBytes(6), 0, 0));
        for (int i = 0; i < 2500; i++) {
            int partitionCount = r.nextInt(1000) + 1;
            byte[] valueToHash = new byte[r.nextInt(1000)];
            r.nextBytes(valueToHash);
            final byte configBytes[] = TheHashinator.getConfigureBytes(partitionCount);
            TheHashinator.initialize(TheHashinator.getConfiguredHashinatorClass(), configBytes);
            int eehash = ee.hashinate(valueToHash, TheHashinator.getCurrentConfig());
            int javahash = TheHashinator.getPartitionForParameter(VoltType.typeFromClass(byte[].class).getValue(),
                    valueToHash);
            if (eehash != javahash) {
                System.out.printf("Mismatched hash of (%s) %d bytes %d partitions => EE: %d Java: %d\n",
                        VoltType.typeFromObject(valueToHash).toSQLString(),
                        valueToHash.length, partitionCount, eehash, javahash);
                partitionCount++;
            }
            assertTrue(eehash < partitionCount);
            assertTrue(eehash >= 0);
            assertEquals(eehash, javahash);
        }
        try { ee.release(); } catch (Exception e) {}
    }

    @Test
    public void testElasticHashinatorPartitionMapping() {
        if (hashinatorType == HashinatorType.LEGACY) return;

        ByteBuffer buf = ByteBuffer.allocate(4 + (8 * 3));

        buf.putInt(3);
        buf.putInt(Integer.MIN_VALUE);
        buf.putInt(0);
        buf.putInt(0);
        buf.putInt(1);
        buf.putInt(Integer.MAX_VALUE);
        buf.putInt(2);

        ElasticHashinator hashinator = new ElasticHashinator(buf.array(), false);

        assertEquals( 0, hashinator.partitionForToken(Integer.MIN_VALUE));
        assertEquals( 0, hashinator.partitionForToken(Integer.MIN_VALUE + 1));

        assertEquals( 1, hashinator.partitionForToken(0));
        assertEquals( 1, hashinator.partitionForToken(1));

        assertEquals( 2, hashinator.partitionForToken(Integer.MAX_VALUE));
        assertEquals( 1, hashinator.partitionForToken(Integer.MAX_VALUE - 1));

        //This used to test wrapping, but we don't wrap anymore, there is always a token at Integer.MIN_VALUE
        buf.clear();
        buf.putInt(3);
        buf.putInt(Integer.MIN_VALUE);
        buf.putInt(0);
        buf.putInt(0);
        buf.putInt(1);
        buf.putInt(Integer.MAX_VALUE - 1);
        buf.putInt(2);

        hashinator = new ElasticHashinator(buf.array(), false);

        assertEquals( 0, hashinator.partitionForToken(Integer.MIN_VALUE));
        assertEquals( 0, hashinator.partitionForToken(Integer.MIN_VALUE + 1));

        assertEquals( 1, hashinator.partitionForToken(0));
        assertEquals( 1, hashinator.partitionForToken(1));

        assertEquals( 2, hashinator.partitionForToken(Integer.MAX_VALUE));
        assertEquals( 2, hashinator.partitionForToken(Integer.MAX_VALUE - 1));
    }

    @Test
    public void testElasticAddPartitions() throws Exception {
        if (hashinatorType == HashinatorType.LEGACY) return;

        ElasticHashinator hashinator = new ElasticHashinator(ElasticHashinator.getConfigureBytes(3,
                ElasticHashinator.DEFAULT_TOTAL_TOKENS), false);

        byte[] newConfig = ElasticHashinator.addPartitions(hashinator, 3);

        assertOnChanges(hashinator.pGetCurrentConfig().configBytes, newConfig, 3, 6);
    }

    private static void assertOnChanges(byte oldConfig[], byte newConfig[], int originalCount, int newCount) throws Exception{

        Map<Integer, Integer> oldTokens = deserializeElasticConfig(oldConfig);
        Map<Integer, Integer> newTokens = deserializeElasticConfig(newConfig);

        SetMultimap<Integer, Integer> iOldTokens = invertTokenMap(oldTokens);
        SetMultimap<Integer, Integer> iNewTokens = invertTokenMap(newTokens);

        //Number of buckets should not change
        assertEquals(oldTokens.size(), newTokens.size());
        //Number of buckets should be correct
        assertEquals(oldTokens.size(), ElasticHashinator.DEFAULT_TOTAL_TOKENS);
        for (int ii = 0; ii < originalCount; ii++) {
            Set<Integer> old = iOldTokens.get(ii);
            Set<Integer> changed = iNewTokens.get(ii);
            //For the existing partitions there should be the same or fewer buckets
            assertTrue(changed.size() <= old.size());
            //All the current buckets should have been from the set that existed originally at that partition
            for (Integer t : changed) {
                assertTrue(old.contains(t));
            }
        }

        for (int ii = originalCount; ii < newCount; ii++) {
            Set<Integer> tokens = iNewTokens.get(ii);
            long count = tokens.size();
            long desired = ElasticHashinator.DEFAULT_TOTAL_TOKENS / newCount;

            //Should have the same or one less than the ideal
            assertTrue(tokens.size() == ElasticHashinator.DEFAULT_TOTAL_TOKENS / newCount ||
                    tokens.size() == ((ElasticHashinator.DEFAULT_TOTAL_TOKENS / newCount) + 1));
            for (Integer t : tokens) {
                assertTrue(oldTokens.containsKey(t));
            }
        }
    }

    @Test
    public void testElasticPredecessors() {
        if (hashinatorType == HashinatorType.LEGACY) return;

        byte[] config = ElasticHashinator.getConfigureBytes(3,
                                                            ElasticHashinator.DEFAULT_TOTAL_TOKENS);
        byte[] newConfig = ElasticHashinator.addPartitions(new ElasticHashinator(config, false),
                                                           3);
        TheHashinator.initialize(HashinatorType.ELASTIC.hashinatorClass, newConfig);
        Map<Integer, Integer> newTokens = deserializeElasticConfig(newConfig);
        Set<Integer> tokensForP4 = new HashSet<Integer>();
        Map<Integer, Integer> tokensToPredecessors = TheHashinator.predecessors(4);
        Set<Integer> predecessors = new HashSet<Integer>(tokensToPredecessors.values());
        // Predecessor set shouldn't contain the partition itself
        assertFalse(predecessors.contains(4));

        for (Map.Entry<Integer, Integer> entry : newTokens.entrySet()) {
            if (tokensToPredecessors.containsKey(entry.getKey())) {
                assertEquals(entry.getValue(), tokensToPredecessors.get(entry.getKey()));
            }
            if (entry.getValue() == 4) {
                tokensForP4.add(entry.getKey());
            }
        }
        assertEquals(ElasticHashinator.DEFAULT_TOTAL_TOKENS / 6, tokensForP4.size());

        ElasticHashinator hashinator = new ElasticHashinator(TheHashinator.getCurrentConfig().configBytes, false);
        for (int token : tokensForP4) {
            int pid;
            if (token != Integer.MIN_VALUE) {
                pid = hashinator.partitionForToken(token - 1);
            } else {
                pid = hashinator.partitionForToken(Integer.MAX_VALUE);
            }
            predecessors.remove(pid);
        }
        assertEquals(0, predecessors.size());
    }

    @Test
    public void testElasticPredecessor() {
        if (hashinatorType == HashinatorType.LEGACY) return;

        byte[] config = ElasticHashinator.getConfigureBytes(3,
                                                            ElasticHashinator.DEFAULT_TOTAL_TOKENS);
        byte[] newConfig = ElasticHashinator.addPartitions(new ElasticHashinator(config, false),
                                                           3);
        TheHashinator.initialize(HashinatorType.ELASTIC.hashinatorClass, newConfig);
        Map<Integer, Integer> predecessors = TheHashinator.predecessors(4);

        // pick the first predecessor
        Map.Entry<Integer, Integer> predecessor = predecessors.entrySet().iterator().next();
        // if token and partition doesn't match, it should throw
        try {
            TheHashinator.predecessor(predecessor.getValue(), predecessor.getKey() - 1);
            fail();
        } catch (Exception e) {}
        Pair<Integer, Integer> prevPredecessor = TheHashinator.predecessor(predecessor.getValue(), predecessor.getKey());
        assertNotNull(prevPredecessor);

        ElasticHashinator hashinator = new ElasticHashinator(TheHashinator.getCurrentConfig().configBytes, false);
        assertEquals(prevPredecessor.getSecond().intValue(), hashinator.partitionForToken(prevPredecessor.getFirst()));
        // check if predecessor's token - 1 belongs to the previous predecessor
        if (predecessor.getKey() != Integer.MIN_VALUE) {
            assertEquals(prevPredecessor.getSecond().intValue(), hashinator.partitionForToken(predecessor.getKey() - 1));
        } else {
            assertEquals(prevPredecessor.getSecond().intValue(), hashinator.partitionForToken(Integer.MAX_VALUE));
        }
    }

    @Test
    public void testElasticAddPartitionDeterminism() {
        if (hashinatorType == HashinatorType.LEGACY) return;

        ElasticHashinator hashinator = new ElasticHashinator(ElasticHashinator.getConfigureBytes(3,
                ElasticHashinator.DEFAULT_TOTAL_TOKENS), false);

        // Add 3 partitions in a batch to the original hashinator
        byte[] batchAddConfig = ElasticHashinator.addPartitions(hashinator, 3);
        Map<Integer, Integer> batchAddTokens = deserializeElasticConfig(batchAddConfig);

        // Add the same 3 partitions one at a time to the original hashinator
        byte[] add3Config = ElasticHashinator.addPartitions(hashinator, 1);
        ElasticHashinator add3Hashinator = new ElasticHashinator(add3Config, false);
        byte[] add4Config = ElasticHashinator.addPartitions(add3Hashinator, 1);
        ElasticHashinator add4Hashinator = new ElasticHashinator(add4Config, false);
        byte[] seqAddConfig = ElasticHashinator.addPartitions(add4Hashinator, 1);
        Map<Integer, Integer> seqAddTokens = deserializeElasticConfig(seqAddConfig);

        // The two approaches should produce the same hash ring
        assertFalse(seqAddTokens.isEmpty());
        assertTrue(seqAddTokens.values().contains(3));
        assertTrue(seqAddTokens.values().contains(4));
        assertTrue(seqAddTokens.values().contains(5));
        assertEquals(batchAddTokens, seqAddTokens);
    }
    @Test
    public void testElasticGetRanges() {
        if (hashinatorType == HashinatorType.LEGACY) return;

        getRangesAndCheck(/* partitionCount = */ 2/* partitionToCheck = */ 1);
        getRangesAndCheck(/* partitionCount = */ 24, /* partitionToCheck = */ 15);
    }

    @Test
    public void testElasticExpansionDeterminism()
    {
        if (hashinatorType == HashinatorType.LEGACY) return;

        checkRangesAfterExpansion(/* beforePartitionCount = */ 2, /* afterPartitionCount = */ 6);
        checkRangesAfterExpansion(/* beforePartitionCount = */ 21, /* afterPartitionCount = */ 28);
        checkRangesAfterExpansion(/* beforePartitionCount = */ 24, /* afterPartitionCount = */ 48);
    }

    @Test
    public void testGetConfigurationSignature()
    {
        final byte configBytes[] = TheHashinator.getConfigureBytes(2);
        TheHashinator.initialize(TheHashinator.getConfiguredHashinatorClass(), configBytes);


        long expected = TheHashinator.computeConfigurationSignature(configBytes);
        assertEquals(expected, TheHashinator.getConfigurationSignature());
    }

    @Test
    public void testSaveRestoreRaw() throws Exception
    {
        if (hashinatorType == HashinatorType.LEGACY) return;

        ElasticHashinator h1 = new ElasticHashinator(
                ElasticHashinator.getConfigureBytes(3,
                ElasticHashinator.DEFAULT_TOTAL_TOKENS), false);
        byte[] bytes = h1.getConfigBytes();
        HashinatorSnapshotData d1 = new HashinatorSnapshotData(bytes, 1234);
        InstanceId iid1 = new InstanceId(111, 222);
        ByteBuffer b1 = d1.saveToBuffer(iid1);
        ByteBuffer b2 = ByteBuffer.wrap(b1.array());
        HashinatorSnapshotData d2 = new HashinatorSnapshotData();
        InstanceId iid2 = d2.restoreFromBuffer(b2);
        assertEquals(iid1, iid2);
        assertTrue(Arrays.equals(d1.m_serData, d2.m_serData));
        ElasticHashinator h2 = new ElasticHashinator(d2.m_serData, false);
        assertEquals(h1.getTokens(), h2.getTokens());
    }

    @Test
    public void testSaveRestoreCooked() throws Exception
    {
        if (hashinatorType == HashinatorType.LEGACY) return;

        ElasticHashinator h1 = new ElasticHashinator(
                ElasticHashinator.getConfigureBytes(3,
                ElasticHashinator.DEFAULT_TOTAL_TOKENS), false);
        byte[] b1 = h1.getCookedBytes();
        ElasticHashinator h2 = new ElasticHashinator(b1, true);
        byte[] b2 = h2.getCookedBytes();
        assertTrue(Arrays.equals(b1, b2));
        assertEquals(h1.getTokens(), h2.getTokens());
    }

    @Test
    public void testAddTokens()
    {
        if (hashinatorType == HashinatorType.LEGACY) return;

        ElasticHashinator dut = new ElasticHashinator(ElasticHashinator.getConfigureBytes(1, 4), false);
        ImmutableSortedMap<Integer, Integer> tokens = dut.getTokens();

        // Add two tokens that don't fall on the bucket boundary, then add one that falls on the boundary.
        // The intermediate tokens should be "moved" forward, so that it doesn't end up having two intermediate
        // tokens at the end.
        dut = addTokenAndCheck(dut, tokens, tokens.firstKey() + 20, 1, true);
        dut = addTokenAndCheck(dut, tokens, tokens.firstKey() + 10, 1, true);
        dut = addTokenAndCheck(dut, tokens, tokens.firstKey(),      1, false);
    }

    private ElasticHashinator addTokenAndCheck(ElasticHashinator dut,
                                               ImmutableSortedMap<Integer, Integer> initialTokens,
                                               int token, int partition, boolean hasIntermediateToken)
    {
        TreeMap<Integer, Integer> tokensToAdd = Maps.newTreeMap();
        tokensToAdd.put(token, partition);
        dut = dut.addTokens(tokensToAdd);
        SortedMapDifference<Integer, Integer> diff = Maps.difference(initialTokens, dut.getTokens());
        assertTrue(diff.entriesOnlyOnLeft().isEmpty());

        if (hasIntermediateToken) {
            assertTrue(diff.entriesDiffering().isEmpty());
            assertEquals(partition, diff.entriesOnlyOnRight().size());
            assertEquals(token, diff.entriesOnlyOnRight().firstKey().intValue());
        } else {
            assertTrue(diff.entriesOnlyOnRight().isEmpty());
            assertEquals(1, diff.entriesDiffering().size());
            assertEquals(initialTokens.firstKey(), diff.entriesDiffering().firstKey());
            assertEquals(partition, diff.entriesDiffering().get(initialTokens.firstKey()).rightValue().intValue());
        }

        return dut;
    }
}
TOP

Related Classes of org.voltdb.TestTheHashinator

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.