/* Copyright (c) 2013, 2017, Oracle and/or its affiliates. All rights reserved. The MySQL Connector/J is licensed under the terms of the GPLv2 <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>, like most MySQL Connectors. There are special exceptions to the terms and conditions of the GPLv2 as it is applied to this software, see the FOSS License Exception <http://www.mysql.com/about/legal/licensing/foss-exception.html>. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; version 2 of the License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ package testsuite.fabric; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import com.mysql.fabric.HashShardMapping; import com.mysql.fabric.RangeShardMapping; import com.mysql.fabric.ShardIndex; import com.mysql.fabric.ShardMapping; import com.mysql.fabric.ShardingType; import junit.framework.TestCase; /** * Tests for shard mappings. */ public class TestShardMapping extends TestCase { /** * Test that a range based shard mapping looks up groups for keys correctly. */ public void testRangeShardMappingKeyLookup() throws Exception { final String globalGroupName = "My global group"; final int lowerBounds[] = new int[] { 1, 10000, 1001, 400, 1000, 470 }; final int lowestLowerBound = 1; // setup the mapping Set<ShardIndex> shardIndices = new HashSet<ShardIndex>(); int shardId = 0; // shard id's added sequentially increasing from 0 for (Integer lowerBound : lowerBounds) { ShardIndex i = new ShardIndex(String.valueOf(lowerBound), shardId, "shard_group_" + shardId); shardId++; shardIndices.add(i); } ShardMapping mapping = new RangeShardMapping(5000, ShardingType.RANGE, globalGroupName, null, shardIndices); // try adding a second shard index with a lower bound that conflicts with an existing one this should be prohibited // mapping.addShardIndex(new ShardIndex(mapping, "1", 0, "")); // test looking up a key out of range doesn't work try { mapping.getGroupNameForKey(String.valueOf(lowestLowerBound - 1)); fail("Looking up a key with a value below the lowest bound is invalid"); } catch (Exception ex) { } try { mapping.getGroupNameForKey(String.valueOf(lowestLowerBound - 1000)); fail("Looking up a key with a value below the lowest bound is invalid"); } catch (Exception ex) { } // test key lookups for lower bound values for (shardId = 0; shardId < lowerBounds.length; ++shardId) { int lowerBound = lowerBounds[shardId]; String groupName = mapping.getGroupNameForKey(String.valueOf(lowerBound)); assertEquals("Exact lookup for key " + lowerBound, "shard_group_" + shardId, groupName); } } public void testHashShardMappingKeyLookup() throws Exception { final String globalGroupName = "My global group"; final String lowerBounds[] = new String[] { /* 0 = */"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", /* 1 = */"66666666666666666666666666666666", /* 2 = */"2809A05A22A4A9C1882A580BCC0AD8A6", /* 3 = */"DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD" }; // setup the mapping Set<ShardIndex> shardIndices = new HashSet<ShardIndex>(); int shardId = 0; // shard id's added sequentially increasing from 0 for (String lowerBound : lowerBounds) { ShardIndex i = new ShardIndex(lowerBound, shardId, "server_group_" + shardId); shardId++; shardIndices.add(i); } ShardMapping mapping = new HashShardMapping(5000, ShardingType.HASH, globalGroupName, null, shardIndices); // test lookups mapping of test value to the group it maps to test values are hashed with MD5 and compared to lowerBounds values String testPairs[][] = new String[][] { // exact match should be in that shard new String[] { "Jess", "server_group_2" }, // hash = 2809a05a22a4a9c1882a580bcc0ad8a6 new String[] { "x", "server_group_1" }, // hash = 9dd4e461268c8034f5c8564e155c67a6 new String[] { "X", "server_group_3" }, // hash = 02129bb861061d1a052c592e2dc6b383 new String[] { "Y", "server_group_2" }, // hash = 57cec4137b614c87cb4e24a3d003a3e0 new String[] { "g", "server_group_0" }, // hash = b2f5ff47436671b6e533d8dc3614845d // leading zeroes new String[] { "168", "server_group_3" }, // hash = 006f52e9102a8d3be2fe5614f42ba989 }; for (String[] testPair : testPairs) { String key = testPair[0]; String serverGroup = testPair[1]; assertEquals(serverGroup, mapping.getGroupNameForKey(key)); } // test a random set of values. we should never return null for (int i = 0; i < 1000; ++i) { assertNotNull(mapping.getGroupNameForKey("" + i)); } } /** * Tests fix for Bug#82203 - com.mysql.fabric.HashShardMapping is not thread safe. * * This test is non-deterministic but most runs used to fail before 5 to 10 seconds. This test runs at most for 30 seconds. */ public void testBug82203() throws Throwable { int numberOfThreads = 2; ExecutorService executorService = Executors.newFixedThreadPool(numberOfThreads); List<Future<?>> resultList = new ArrayList<Future<?>>(); for (int i = 0; i < numberOfThreads; i++) { resultList.add(executorService.submit(new TestBug82203RunnableMock(30))); } for (Future<?> f : resultList) { try { f.get(); } catch (ExecutionException e) { if (e.getCause() != null) { throw e.getCause(); } throw e; } } executorService.shutdown(); } private static class TestBug82203RunnableMock extends HashShardMapping implements Runnable { private static volatile boolean run = true; private long time; public TestBug82203RunnableMock(int secs) { super(0, null, null, null, Collections.singleton(new ShardIndex("", 1, ""))); this.time = TimeUnit.SECONDS.toMillis(secs); } public void run() { try { long now = System.currentTimeMillis(); while (run && System.currentTimeMillis() - now < this.time) { int id = ((int) (Math.random() * 100)) % 100 + 1; String key = makeKey(id); getShardIndexForKey(key); } } catch (Exception e) { fail("Due to: " + e); } finally { run = false; } } private String makeKey(int len) { char[] chars = new char[len]; for (int i = 0; i < len; i++) { int r = ((int) (Math.random() * 100)) % 52; chars[i] = (char) (r < 26 ? 'a' + r : 'A' + r - 26); } return String.valueOf(chars); } } }