/*
* Copyright © 2014-2015 Cask Data, Inc.
*
* 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 co.cask.cdap.data2.dataset2.lib.table;
import co.cask.cdap.api.common.Bytes;
import co.cask.cdap.api.dataset.table.Row;
import co.cask.cdap.api.dataset.table.Scanner;
import co.cask.cdap.common.utils.ImmutablePair;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Maps;
import org.junit.Assert;
import org.junit.Test;
import java.io.Closeable;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
* tests metrics table.
*/
public abstract class MetricsTableTest {
protected abstract MetricsTable getTable(String name) throws Exception;
protected static final byte[] A = Bytes.toBytes(1L);
protected static final byte[] B = Bytes.toBytes(2L);
protected static final byte[] C = Bytes.toBytes(3L);
protected static final byte[] P = Bytes.toBytes(4L);
protected static final byte[] Q = Bytes.toBytes(5L);
protected static final byte[] R = Bytes.toBytes(6L);
protected static final byte[] X = Bytes.toBytes(7L);
protected static final byte[] Y = Bytes.toBytes(8L);
protected static final byte[] Z = Bytes.toBytes(9L);
@Test
public void testGetPutSwap() throws Exception {
MetricsTable table = getTable("testGetPutSwap");
// put two rows
table.put(ImmutableSortedMap.<byte[], SortedMap<byte[], Long>>orderedBy(Bytes.BYTES_COMPARATOR)
.put(A, mapOf(P, Bytes.toLong(X), Q, Bytes.toLong(Y)))
.put(B, mapOf(P, Bytes.toLong(X), R, Bytes.toLong(Z))).build());
Assert.assertEquals(Bytes.toLong(X), Bytes.toLong(table.get(A, P)));
Assert.assertEquals(Bytes.toLong(Y), Bytes.toLong(table.get(A, Q)));
Assert.assertNull(table.get(A, R));
Assert.assertEquals(Bytes.toLong(X), Bytes.toLong(table.get(B, P)));
Assert.assertEquals(Bytes.toLong(Z), Bytes.toLong(table.get(B, R)));
Assert.assertNull(table.get(B, Q));
// put one row again, with overlapping key set
table.put(ImmutableSortedMap.<byte[], SortedMap<byte[], Long>>orderedBy(Bytes.BYTES_COMPARATOR)
.put(A, mapOf(P, Bytes.toLong(A), R, Bytes.toLong(C))).build());
Assert.assertEquals(Bytes.toLong(A), Bytes.toLong(table.get(A, P)));
Assert.assertEquals(Bytes.toLong(Y), Bytes.toLong(table.get(A, Q)));
Assert.assertEquals(Bytes.toLong(C), Bytes.toLong(table.get(A, R)));
// compare and swap an existing value, successfully
Assert.assertTrue(table.swap(A, P, A, B));
Assert.assertEquals(Bytes.toLong(B), Bytes.toLong(table.get(A, P)));
// compare and swap an existing value that does not match
Assert.assertFalse(table.swap(A, P, A, B));
Assert.assertArrayEquals(B, table.get(A, P));
// compare and swap an existing value that does not exist
Assert.assertFalse(table.swap(B, Q, A, B));
Assert.assertNull(table.get(B, Q));
// compare and delete an existing value, successfully
Assert.assertTrue(table.swap(A, P, B, null));
Assert.assertNull(table.get(A, P));
// compare and delete an existing value that does not match
Assert.assertFalse(table.swap(A, Q, A, null));
Assert.assertArrayEquals(Y, table.get(A, Q));
// compare and swap a null value, successfully
Assert.assertTrue(table.swap(A, P, null, Z));
Assert.assertArrayEquals(Z, table.get(A, P));
// compare and swap a null value, successfully
Assert.assertFalse(table.swap(A, Q, null, Z));
Assert.assertArrayEquals(Y, table.get(A, Q));
}
protected class IncThread extends Thread implements Closeable {
final MetricsTable table;
final byte[] row;
final Map<byte[], Long> incrememts;
int rounds;
public IncThread(MetricsTable table, byte[] row, Map<byte[], Long> incrememts, int rounds) {
this.table = table;
this.row = row;
this.incrememts = incrememts;
this.rounds = rounds;
}
public void run() {
while (rounds-- > 0) {
try {
table.increment(row, incrememts);
} catch (Exception e) {
System.err.println("exception for increment #" + rounds + ": " + e.getMessage());
e.printStackTrace();
}
}
}
@Override
public void close() throws IOException {
table.close();
}
}
protected class IncAndGetThread extends Thread implements Closeable {
final MetricsTable table;
final byte[] row;
final byte[] col;
final long delta;
int rounds;
Long previous = 0L;
public IncAndGetThread(MetricsTable table, byte[] row, byte[] col, long delta, int rounds) {
this.table = table;
this.row = row;
this.col = col;
this.delta = delta;
this.rounds = rounds;
}
public void run() {
while (rounds-- > 0) {
try {
long value = table.incrementAndGet(row, col, delta);
Assert.assertTrue(value > previous);
} catch (Exception e) {
System.err.println("exception for increment and get #" + rounds + ": " + e.getMessage());
e.printStackTrace();
}
}
}
@Override
public void close() throws IOException {
table.close();
}
}
@Test
public void testConcurrentIncrement() throws Exception {
final MetricsTable table = getTable("testConcurrentIncrement");
final int rounds = 500;
Map<byte[], Long> inc1 = ImmutableMap.of(X, 1L, Y, 2L);
Map<byte[], Long> inc2 = ImmutableMap.of(Y, 1L, Z, 2L);
Collection<? extends Thread> threads = ImmutableList.of(new IncThread(table, A, inc1, rounds),
new IncThread(table, A, inc2, rounds),
new IncAndGetThread(table, A, Z, 5, rounds),
new IncAndGetThread(table, A, Z, 2, rounds));
for (Thread t : threads) {
t.start();
}
for (Thread t : threads) {
t.join();
if (t instanceof Closeable) {
((Closeable) t).close();
}
}
Assert.assertEquals(rounds + 10L, table.incrementAndGet(A, X, 10L));
Assert.assertEquals(3 * rounds - 20L, table.incrementAndGet(A, Y, -20L));
Assert.assertEquals(9 * rounds, table.incrementAndGet(A, Z, 0L));
}
class SwapThread extends Thread {
private final MetricsTable table;
private final byte[] row;
private final byte[] col;
private final AtomicInteger[] counts;
private final long rounds;
SwapThread(MetricsTable table, byte[] row, byte[] col, AtomicInteger[] counts, long rounds) {
this.table = table;
this.row = row;
this.col = col;
this.counts = counts;
this.rounds = rounds;
}
public void run() {
for (long i = 0; i < rounds; i++) {
try {
boolean success = table.swap(row, col, Bytes.toBytes(i), Bytes.toBytes(i + 1));
counts[success ? 0 : 1].incrementAndGet();
} catch (Exception e) {
System.err.println("exception for swap #" + rounds + ": " + e.getMessage());
e.printStackTrace();
}
}
}
}
@Test
public void testConcurrentSwap() throws Exception {
MetricsTable table = getTable("testConcurrentSwap");
final long rounds = 500;
table.put(ImmutableSortedMap.<byte[], SortedMap<byte[], Long>>orderedBy(Bytes.BYTES_COMPARATOR)
.put(A, mapOf(B, 0L)).build());
AtomicInteger[] counts = { new AtomicInteger(), new AtomicInteger() }; // [0] for success, [1] for failures
Thread t1 = new SwapThread(table, A, B, counts, rounds);
Thread t2 = new SwapThread(table, A, B, counts, rounds);
t1.start();
t2.start();
t1.join();
t2.join();
Assert.assertEquals(rounds, Bytes.toLong(table.get(A, B)));
Assert.assertEquals(rounds, counts[0].get()); // number of successful swaps
Assert.assertEquals(rounds, counts[1].get()); // number of failed swaps
}
@Test
public void testDelete() throws Exception {
MetricsTable table = getTable("testDelete");
NavigableMap<byte[], SortedMap<byte[], Long>> writes = Maps.newTreeMap(Bytes.BYTES_COMPARATOR);
for (int i = 0; i < 1024; i++) {
writes.put(Bytes.toBytes(i << 22), mapOf(A, Bytes.toLong(X)));
}
table.put(writes);
// verify the first and last are there (sanity test for correctness of test logic)
Assert.assertArrayEquals(X, table.get(Bytes.toBytes(0x00000000), A));
Assert.assertArrayEquals(X, table.get(Bytes.toBytes(0xffc00000), A));
List<byte[]> toDelete = ImmutableList.of(
Bytes.toBytes(0xa1000000), Bytes.toBytes(0xb2000000), Bytes.toBytes(0xc3000000));
// verify these three are there, and delete them
for (byte[] row : toDelete) {
Assert.assertArrayEquals(X, table.get(row, A));
table.delete(row, new byte[][] {A});
}
// verify these three are now gone.
for (byte[] row : toDelete) {
Assert.assertNull(table.get(row, A));
}
// verify nothing else is gone by counting all entries in a scan
Assert.assertEquals(1021, countRange(table, null, null));
}
@Test
public void testDeleteIncrements() throws Exception {
// note: this is pretty important test case for tables with counters, e.g. metrics
MetricsTable table = getTable("testDeleteIncrements");
// delete increment and increment again
table.increment(A, ImmutableMap.of(B, 5L));
table.increment(A, ImmutableMap.of(B, 2L));
Assert.assertEquals(7L, Bytes.toLong(table.get(A, B)));
table.delete(A, new byte[][]{B});
Assert.assertNull(table.get(A, B));
table.increment(A, ImmutableMap.of(B, 3L));
Assert.assertEquals(3L, Bytes.toLong(table.get(A, B)));
}
private static int countRange(MetricsTable table, Integer start, Integer stop) throws Exception {
Scanner scanner = table.scan(start == null ? null : Bytes.toBytes(start),
stop == null ? null : Bytes.toBytes(stop), null);
int count = 0;
while (scanner.next() != null) {
count++;
}
return count;
}
@Test
public void testFuzzyScan() throws Exception {
MetricsTable table = getTable("testFuzzyScan");
NavigableMap<byte[], SortedMap<byte[], Long>> writes = Maps.newTreeMap(Bytes.BYTES_COMPARATOR);
byte[] abc = { 'a', 'b', 'c' };
for (byte b1 : abc) {
for (byte b2 : abc) {
for (byte b3 : abc) {
for (byte b4 : abc) {
// we put two columns, but will scan only one column
writes.put(new byte[] { b1, b2, b3, b4 }, mapOf(A, Bytes.toLong(X)));
}
}
}
}
table.put(writes);
// we should have 81 (3^4) rows now
Assert.assertEquals(81, countRange(table, null, null));
// now do a fuzzy scan of the table
FuzzyRowFilter filter = new FuzzyRowFilter(
ImmutableList.of(ImmutablePair.of(new byte[] { '*', 'b', '*', 'b' }, new byte[] { 0x01, 0x00, 0x01, 0x00 })));
Scanner scanner = table.scan(null, null, filter);
int count = 0;
while (true) {
Row entry = scanner.next();
if (entry == null) {
break;
}
Assert.assertTrue(entry.getRow()[1] == 'b' && entry.getRow()[3] == 'b');
Assert.assertEquals(1, entry.getColumns().size());
Assert.assertTrue(entry.getColumns().containsKey(A));
count++;
}
Assert.assertEquals(9, count);
}
private <T> SortedMap<byte[], T> mapOf(byte[] key, T value) {
SortedMap<byte[], T> map = new TreeMap<>(Bytes.BYTES_COMPARATOR);
map.put(key, value);
return map;
}
private <T> SortedMap<byte[], T> mapOf(byte[] firstKey, T firstValue, byte[] secondKey, T secondValue) {
SortedMap<byte[], T> map = new TreeMap<>(Bytes.BYTES_COMPARATOR);
map.put(firstKey, firstValue);
map.put(secondKey, secondValue);
return map;
}
}