package org.gbif.occurrence.persistence.keygen; import org.gbif.occurrence.common.config.OccHBaseConfiguration; import org.gbif.occurrence.persistence.IllegalDataStateException; import org.gbif.occurrence.persistence.api.KeyLookupResult; import org.gbif.occurrence.persistence.hbase.Columns; import java.io.IOException; import java.util.List; import java.util.Set; import java.util.UUID; import java.util.concurrent.TimeUnit; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import org.apache.hadoop.hbase.HBaseTestingUtility; import org.apache.hadoop.hbase.TableName; import org.apache.hadoop.hbase.client.Connection; import org.apache.hadoop.hbase.client.ConnectionFactory; import org.apache.hadoop.hbase.client.Put; import org.apache.hadoop.hbase.client.Table; import org.apache.hadoop.hbase.util.Bytes; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; //@Ignore("As per http://dev.gbif.org/issues/browse/OCC-109") public class HBaseLockingKeyServiceTest { private static final String A = "a"; private static final String B = "b"; private static final String C = "c"; private static final OccHBaseConfiguration CFG = new OccHBaseConfiguration(); static { CFG.setEnvironment("test"); } private static final byte[] LOOKUP_TABLE = Bytes.toBytes(CFG.lookupTable); private static final String CF_NAME = "o"; private static final byte[] CF = Bytes.toBytes(CF_NAME); private static final byte[] COUNTER_TABLE = Bytes.toBytes(CFG.counterTable); private static final String COUNTER_CF_NAME = "o"; private static final byte[] COUNTER_CF = Bytes.toBytes(COUNTER_CF_NAME); private static final byte[] OCCURRENCE_TABLE = Bytes.toBytes(CFG.occTable); private static Connection CONNECTION = null; private static final HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility(); private HBaseLockingKeyService keyService; private OccurrenceKeyBuilder keyBuilder = new OccurrenceKeyBuilder(); @Rule public ExpectedException exception = ExpectedException.none(); @BeforeClass public static void beforeClass() throws Exception { TEST_UTIL.startMiniCluster(1); TEST_UTIL.createTable(LOOKUP_TABLE, CF); TEST_UTIL.createTable(COUNTER_TABLE, COUNTER_CF); TEST_UTIL.createTable(OCCURRENCE_TABLE, CF); CONNECTION = ConnectionFactory.createConnection(TEST_UTIL.getConfiguration()); } @Before public void before() throws IOException { TEST_UTIL.truncateTable(LOOKUP_TABLE); TEST_UTIL.truncateTable(COUNTER_TABLE); TEST_UTIL.truncateTable(OCCURRENCE_TABLE); keyService = new HBaseLockingKeyService(CFG, CONNECTION); } @AfterClass public static void afterClass() throws Exception { TEST_UTIL.shutdownMiniCluster(); CONNECTION.close(); } @Test public void testNoContention() { Set<String> uniqueIds = Sets.newHashSet(); uniqueIds.add(A); uniqueIds.add(B); uniqueIds.add(C); KeyLookupResult result = keyService.generateKey(uniqueIds, "boo"); assertEquals(1, result.getKey()); assertTrue(result.isCreated()); KeyLookupResult result2 = keyService.findKey(uniqueIds, "boo"); assertEquals(1, result2.getKey()); assertFalse(result2.isCreated()); } @Test public void testAddOccIdToExistingTriplet() throws IOException { // setup: 1 finalized row, the triplet String datasetKey = UUID.randomUUID().toString(); String triplet = "IC|CC|CN|null"; byte[] lookupKey1 = Bytes.toBytes(keyBuilder.buildKey(triplet, datasetKey)); Put put = new Put(lookupKey1); put.addColumn(CF, Bytes.toBytes(Columns.LOOKUP_STATUS_COLUMN), Bytes.toBytes("ALLOCATED")); put.addColumn(CF, Bytes.toBytes(Columns.LOOKUP_KEY_COLUMN), Bytes.toBytes(2)); try(Table lookupTable = CONNECTION.getTable(TableName.valueOf(LOOKUP_TABLE))) { lookupTable.put(put); } // test: keygen attempt uses previous unique id and a new "occurrenceId", expects the existing key to be returned KeyLookupResult result = keyService.generateKey(ImmutableSet.of(triplet, "ABCD"), datasetKey); assertEquals(2, result.getKey()); assertFalse(result.isCreated()); } @Test public void testSimpleIdContig() { KeyLookupResult result = null; for (int i = 0; i < 1000; i++) { Set<String> uniqueIds = ImmutableSet.of(String.valueOf(i)); result = keyService.generateKey(uniqueIds, "boo"); } assertEquals(1000, result.getKey()); } @Test public void testResumeCountAfterFailure() { KeyLookupResult result = null; for (int i = 0; i < 250; i++) { Set<String> uniqueIds = ImmutableSet.of(String.valueOf(i)); result = keyService.generateKey(uniqueIds, "boo"); } assertEquals(250, result.getKey()); // first one claimed up to 300, then "died". On restart we claim 300 to 400. HBaseLockingKeyService keyService2 = new HBaseLockingKeyService(CFG, CONNECTION); for (int i = 0; i < 50; i++) { Set<String> uniqueIds = ImmutableSet.of("A" + i); result = keyService2.generateKey(uniqueIds, "boo"); } assertEquals(350, result.getKey()); } @Test public void testLockWriteDie() throws IOException { // setup: 2 rows, each one gets as far as writing the new id but "dies" before releasing lock String datasetKey = UUID.randomUUID().toString(); byte[] lock1 = Bytes.toBytes(UUID.randomUUID().toString()); byte[] lookupKey1 = Bytes.toBytes(datasetKey + "|ABCD"); Put put = new Put(lookupKey1); put.addColumn(CF, Bytes.toBytes(Columns.LOOKUP_LOCK_COLUMN), 0, lock1); put.addColumn(CF, Bytes.toBytes(Columns.LOOKUP_KEY_COLUMN), Bytes.toBytes(2)); try(Table lookupTable = CONNECTION.getTable(TableName.valueOf(LOOKUP_TABLE))) { lookupTable.put(put); byte[] lock2 = Bytes.toBytes(UUID.randomUUID().toString()); byte[] lookupKey2 = Bytes.toBytes(datasetKey + "|EFGH"); put = new Put(lookupKey2); put.addColumn(CF, Bytes.toBytes(Columns.LOOKUP_LOCK_COLUMN), 0, lock2); put.addColumn(CF, Bytes.toBytes(Columns.LOOKUP_KEY_COLUMN), Bytes.toBytes(3)); lookupTable.put(put); lookupTable.close(); } // test: 3rd keygen attempt uses both previous unique ids, expects a new key to be generated KeyLookupResult result = keyService.generateKey(ImmutableSet.of("ABCD", "EFGH"), datasetKey); assertEquals(1, result.getKey()); } @Test public void testConflictingIds() throws IOException { // setup: 2 rows with different lookupkeys and assigned ids String datasetKey = "fakeuuid"; byte[] lookupKey1 = Bytes.toBytes(datasetKey + "|ABCD"); Put put = new Put(lookupKey1); put.addColumn(CF, Bytes.toBytes(Columns.LOOKUP_STATUS_COLUMN), Bytes.toBytes("ALLOCATED")); put.addColumn(CF, Bytes.toBytes(Columns.LOOKUP_KEY_COLUMN), Bytes.toBytes(1)); try (Table lookupTable = CONNECTION.getTable(TableName.valueOf(LOOKUP_TABLE))) { lookupTable.put(put); byte[] lookupKey2 = Bytes.toBytes(datasetKey + "|EFGH"); put = new Put(lookupKey2); put.addColumn(CF, Bytes.toBytes(Columns.LOOKUP_STATUS_COLUMN), Bytes.toBytes("ALLOCATED")); put.addColumn(CF, Bytes.toBytes(Columns.LOOKUP_KEY_COLUMN), Bytes.toBytes(2)); lookupTable.put(put); lookupTable.close(); } // test: gen id for one occ with both lookupkeys exception.expect(IllegalDataStateException.class); exception.expectMessage( "Found inconsistent occurrence keys in looking up unique identifiers:[" + datasetKey + "|ABCD]=[1][" + datasetKey + "|EFGH]=[2]"); keyService.generateKey(ImmutableSet.of("ABCD", "EFGH"), datasetKey); } @Test public void testStaleLock() throws IOException { String datasetKey = UUID.randomUUID().toString(); // setup: lookupkey | null for status | uuid with old ts for lock | null for occurrence key byte[] lookupKey = Bytes.toBytes(datasetKey + "|ABCD"); byte[] lock = Bytes.toBytes(UUID.randomUUID().toString()); Put put = new Put(lookupKey); put.addColumn(CF, Bytes.toBytes(Columns.LOOKUP_LOCK_COLUMN), 0, lock); try (Table lookupTable = CONNECTION.getTable(TableName.valueOf(LOOKUP_TABLE))) { lookupTable.put(put); } KeyLookupResult result = keyService.generateKey(ImmutableSet.of("ABCD"), datasetKey); assertEquals(1, result.getKey()); } @Test public void testValidLockBecomesStale() throws IOException { String datasetKey = UUID.randomUUID().toString(); // setup: lookupkey | null for status | uuid with old ts for lock | null for occurrence key // now minus the stale timeout + 10 seconds to force retry long ts = System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(5) + TimeUnit.SECONDS.toMillis(10); byte[] lookupKey = Bytes.toBytes(datasetKey + "|ABCD"); byte[] lock = Bytes.toBytes(UUID.randomUUID().toString()); Put put = new Put(lookupKey); put.addColumn(CF, Bytes.toBytes(Columns.LOOKUP_LOCK_COLUMN), ts, lock); try (Table lookupTable = CONNECTION.getTable(TableName.valueOf(LOOKUP_TABLE))) { lookupTable.put(put); } // System.out.println("start at [" + System.currentTimeMillis() + "]"); KeyLookupResult result = keyService.generateKey(ImmutableSet.of("ABCD"), datasetKey); assertEquals(1, result.getKey()); // System.out.println("end at [" + System.currentTimeMillis() + "]"); } @Test public void testThreadedIdContig() throws InterruptedException { // 5 threads concurrently allocated 1000 ids each, expect the next call to produce id 5001 List<Thread> threads = Lists.newArrayList(); for (int i = 0; i < 5; i++) { Thread thread = new Thread(new KeyRequester(1000, keyService, String.valueOf(i))); thread.start(); threads.add(thread); } for (Thread thread : threads) { thread.join(); } KeyLookupResult result = keyService.generateKey(ImmutableSet.of("asdf"), "wqer"); assertEquals(5001, result.getKey()); } private static class KeyRequester implements Runnable { private final int keyCount; private final HBaseLockingKeyService keyService; private final String name; private KeyRequester(int keyCount, HBaseLockingKeyService keyService, String name) { this.keyCount = keyCount; this.keyService = keyService; this.name = name; } @Override public void run() { for (int i = 0; i < keyCount; i++) { Set<String> uniqueIds = ImmutableSet.of(name + i); keyService.generateKey(uniqueIds, "boo"); } } } }