/*
* 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.Get;
import co.cask.cdap.api.dataset.table.Row;
import co.cask.cdap.api.dataset.table.Table;
import co.cask.tephra.DefaultTransactionExecutor;
import co.cask.tephra.TransactionAware;
import co.cask.tephra.TransactionConflictException;
import co.cask.tephra.TransactionExecutor;
import co.cask.tephra.TransactionExecutorFactory;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* This test emulates usage table by multiple concurrent clients.
* @param <T> table type
*/
public abstract class TableConcurrentTest<T extends Table>
extends TableTest<T> {
private static final Logger LOG = LoggerFactory.getLogger(TableConcurrentTest.class);
private static final byte[] ROW_TO_INCREMENT = Bytes.toBytes("row_to_increment");
private static final byte[] COLUMN_TO_INCREMENT = Bytes.toBytes("column_to_increment");
private static final byte[][] ROWS_TO_APPEND_TO;
static {
ROWS_TO_APPEND_TO = new byte[6][];
ROWS_TO_APPEND_TO[0] = ROW_TO_INCREMENT;
for (int i = 1; i < ROWS_TO_APPEND_TO.length; i++) {
ROWS_TO_APPEND_TO[i] = Bytes.toBytes("row_to_append_to_" + i);
}
}
protected TransactionExecutorFactory txExecutorFactory;
@Before
public void before() {
super.before();
txExecutorFactory = new TransactionExecutorFactory() {
@Override
public TransactionExecutor createExecutor(Iterable<TransactionAware> txAwares) {
return new DefaultTransactionExecutor(txClient, txAwares);
}
};
}
@Test(timeout = 120000)
public void testConcurrentOnSingleTable() throws Exception {
// Set of clients read and write data concurrently.
// * n clients increment a value with increasing values (+1, +2, ...) at specific row:column 100 times
// * n clients append 100 columns to a set of 4 rows which includes the row that gets incremented (2 at a time).
// Append is: read all columns, add <last_column+1>
// todo: improve to use deletes. E.g. in append - remove all existing before appending new
final int n = 5;
getTableAdmin(CONTEXT1, MY_TABLE).create();
try {
ExecutorService executor = Executors.newFixedThreadPool(n * 2);
// start threads
for (int i = 0; i < n; i++) {
executor.submit(new IncrementingClient(txExecutorFactory));
executor.submit(new AppendingClient(txExecutorFactory));
}
// wait for finish
executor.shutdown();
executor.awaitTermination(2, TimeUnit.MINUTES);
// verify result
final T table = getTable(CONTEXT1, MY_TABLE);
TransactionExecutor txExecutor =
txExecutorFactory.createExecutor(Lists.newArrayList((TransactionAware) table));
txExecutor.execute(new TransactionExecutor.Subroutine() {
@Override
public void apply() throws Exception {
verifyIncrements();
verifyAppends();
}
private void verifyAppends() throws Exception {
for (byte[] row : ROWS_TO_APPEND_TO) {
Map<byte[], byte[]> cols = table.get(row).getColumns();
Assert.assertFalse(cols.isEmpty());
// +1 because there was one extra column that we incremented
boolean isIncrementedColumn = Arrays.equals(ROW_TO_INCREMENT, row);
Assert.assertEquals(n * 100 + (isIncrementedColumn ? 1 : 0), cols.size());
for (int i = 0; i < n * 100; i++) {
Assert.assertArrayEquals(Bytes.toBytes("foo" + i), cols.get(Bytes.toBytes("column" + i)));
}
}
}
private void verifyIncrements() throws Exception {
Map<byte[], byte[]> result = table.get(ROW_TO_INCREMENT, new byte[][]{COLUMN_TO_INCREMENT}).getColumns();
Assert.assertFalse(result.isEmpty());
byte[] val = result.get(COLUMN_TO_INCREMENT);
long sum1to100 = ((1 + 99) * 99 / 2);
Assert.assertEquals(n * sum1to100, Bytes.toLong(val));
}
});
} finally {
getTableAdmin(CONTEXT1, MY_TABLE).drop();
}
}
private class IncrementingClient implements Runnable {
private final TransactionExecutorFactory txExecutorFactory;
private final T table;
public IncrementingClient(TransactionExecutorFactory txExecutorFactory) throws Exception {
this.txExecutorFactory = txExecutorFactory;
this.table = getTable(CONTEXT1, MY_TABLE);
}
@Override
public void run() {
final int[] executed = {0};
while (executed[0] < 100) {
TransactionExecutor txExecutor =
txExecutorFactory.createExecutor(Lists.newArrayList((TransactionAware) table));
try {
txExecutor.execute(new TransactionExecutor.Subroutine() {
@Override
public void apply() throws Exception {
table.incrementAndGet(ROW_TO_INCREMENT,
new byte[][]{COLUMN_TO_INCREMENT},
new long[]{(long) executed[0]});
}
});
} catch (TransactionConflictException t) {
// LOG.warn("conflict on increment, will retry again");
// do nothing: we'll retry
continue;
} catch (Throwable t) {
LOG.warn("failed to increment, bailing out", t);
throw Throwables.propagate(t);
}
executed[0]++;
}
}
}
private class AppendingClient implements Runnable {
private final TransactionExecutorFactory txExecutorFactory;
private final T table;
public AppendingClient(TransactionExecutorFactory txExecutorFactory) throws Exception {
this.txExecutorFactory = txExecutorFactory;
this.table = getTable(CONTEXT1, MY_TABLE);
}
@Override
public void run() {
// append to ith and (i+1)th rows at the same time
TransactionExecutor txExecutor =
txExecutorFactory.createExecutor(Lists.newArrayList((TransactionAware) table));
for (int k = 0; k < 100; k++) {
for (int i = 0; i < ROWS_TO_APPEND_TO.length / 2; i++) {
final List<Get> gets = ImmutableList.of(new Get(ROWS_TO_APPEND_TO[i * 2]),
new Get(ROWS_TO_APPEND_TO[i * 2 + 1]));
boolean appended = false;
while (!appended) {
try {
txExecutor.execute(new TransactionExecutor.Subroutine() {
@Override
public void apply() throws Exception {
List<Row> rows = table.get(gets);
for (int i = 0; i < gets.size(); i++) {
appendColumn(gets.get(i).getRow(), rows.get(i).getColumns());
}
}
private void appendColumn(byte[] row, Map<byte[], byte[]> columns) throws Exception {
int columnsCount;
if (columns.isEmpty()) {
columnsCount = 0;
} else if (!columns.containsKey(COLUMN_TO_INCREMENT)) {
columnsCount = columns.size();
} else {
// when counting columns, ignore the increment column
columnsCount = columns.size() - 1;
}
byte[] columnToAppend = Bytes.toBytes("column" + columnsCount);
table.put(row, new byte[][]{columnToAppend}, new byte[][] { Bytes.toBytes("foo" + columnsCount) });
}
});
} catch (TransactionConflictException t) {
// LOG.warn("conflict on append, will retry again");
// do nothing: we'll retry
appended = false;
continue;
} catch (Throwable t) {
LOG.warn("failed to append, bailing out", t);
throw Throwables.propagate(t);
}
appended = true;
}
}
}
}
}
/**
* tests that creating a table concurrently from two different clients does not fail.
*/
@Test(timeout = 20000) // table create wait time is 5 sec
public void testConcurrentCreate() throws Exception {
AtomicBoolean success1 = new AtomicBoolean(false);
AtomicBoolean success2 = new AtomicBoolean(false);
// start two threads both attempting to create the same table
Thread t1 = new CreateThread(success1);
Thread t2 = new CreateThread(success2);
t1.start();
t2.start();
t1.join();
t2.join();
// make sure both threads report success
Assert.assertTrue("First thread failed. ", success1.get());
Assert.assertTrue("Second thread failed. ", success2.get());
// perform a read - if the table was not opened successfully this will fail
getTable(CONTEXT1, "conccreate").get(new byte[]{'a'}, new byte[][]{{'b'}});
}
class CreateThread extends Thread {
private final AtomicBoolean success;
CreateThread(AtomicBoolean success) {
this.success = success;
}
@Override
public void run() {
try {
success.set(false);
getTableAdmin(CONTEXT1, "conccreate").create();
success.set(true);
} catch (Throwable throwable) {
success.set(false);
throwable.printStackTrace(System.err);
}
}
}
}