/*
* 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.hbase;
import co.cask.cdap.api.common.Bytes;
import co.cask.cdap.api.dataset.DataSetException;
import co.cask.cdap.api.dataset.DatasetContext;
import co.cask.cdap.api.dataset.DatasetSpecification;
import co.cask.cdap.api.dataset.table.Scanner;
import co.cask.cdap.common.utils.ImmutablePair;
import co.cask.cdap.data2.dataset2.lib.table.FuzzyRowFilter;
import co.cask.cdap.data2.dataset2.lib.table.MetricsTable;
import co.cask.cdap.data2.util.TableId;
import co.cask.cdap.data2.util.hbase.DeleteBuilder;
import co.cask.cdap.data2.util.hbase.HBaseTableUtil;
import co.cask.cdap.data2.util.hbase.PutBuilder;
import co.cask.cdap.data2.util.hbase.ScanBuilder;
import com.google.common.collect.Lists;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.client.Delete;
import org.apache.hadoop.hbase.client.Get;
import org.apache.hadoop.hbase.client.HTable;
import org.apache.hadoop.hbase.client.Increment;
import org.apache.hadoop.hbase.client.Put;
import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.client.ResultScanner;
import org.apache.hadoop.hbase.util.Pair;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.SortedMap;
import javax.annotation.Nullable;
/**
* An HBase metrics table client.
*/
public class HBaseMetricsTable implements MetricsTable {
private final HBaseTableUtil tableUtil;
private final TableId tableId;
private final HTable hTable;
private final byte[] columnFamily;
public HBaseMetricsTable(DatasetContext datasetContext, DatasetSpecification spec,
Configuration hConf, HBaseTableUtil tableUtil) throws IOException {
this.tableUtil = tableUtil;
this.tableId = TableId.from(datasetContext.getNamespaceId(), spec.getName());
HTable hTable = tableUtil.createHTable(hConf, tableId);
// todo: make configurable
hTable.setWriteBufferSize(HBaseTableUtil.DEFAULT_WRITE_BUFFER_SIZE);
hTable.setAutoFlush(false);
this.hTable = hTable;
this.columnFamily = HBaseTableAdmin.getColumnFamily(spec);
}
@Override
@Nullable
public byte[] get(byte[] row, byte[] column) {
try {
Get get = tableUtil.buildGet(row)
.addColumn(columnFamily, column)
.setMaxVersions(1)
.build();
Result getResult = hTable.get(get);
if (!getResult.isEmpty()) {
return getResult.getValue(columnFamily, column);
}
return null;
} catch (IOException e) {
throw new DataSetException("Get failed on table " + tableId, e);
}
}
@Override
public void put(SortedMap<byte[], ? extends SortedMap<byte[], Long>> updates) {
List<Put> puts = Lists.newArrayList();
for (Map.Entry<byte[], ? extends SortedMap<byte[], Long>> row : updates.entrySet()) {
PutBuilder put = tableUtil.buildPut(row.getKey());
for (Map.Entry<byte[], Long> column : row.getValue().entrySet()) {
put.add(columnFamily, column.getKey(), Bytes.toBytes(column.getValue()));
}
puts.add(put.build());
}
try {
hTable.put(puts);
hTable.flushCommits();
} catch (IOException e) {
throw new DataSetException("Put failed on table " + tableId, e);
}
}
@Override
public boolean swap(byte[] row, byte[] column, byte[] oldValue, byte[] newValue) {
try {
if (newValue == null) {
// HBase API weirdness: we must use deleteColumns() because deleteColumn() deletes only the last version.
Delete delete = tableUtil.buildDelete(row)
.deleteColumns(columnFamily, column)
.build();
return hTable.checkAndDelete(row, columnFamily, column, oldValue, delete);
} else {
Put put = tableUtil.buildPut(row)
.add(columnFamily, column, newValue)
.build();
return hTable.checkAndPut(row, columnFamily, column, oldValue, put);
}
} catch (IOException e) {
throw new DataSetException("Swap failed on table " + tableId, e);
}
}
@Override
public void increment(byte[] row, Map<byte[], Long> increments) {
Put increment = getIncrementalPut(row, increments);
try {
hTable.put(increment);
hTable.flushCommits();
} catch (IOException e) {
// figure out whether this is an illegal increment
// currently there is not other way to extract that from the HBase exception than string match
if (e.getMessage() != null && e.getMessage().contains("isn't 64 bits wide")) {
throw new NumberFormatException("Attempted to increment a value that is not convertible to long," +
" row: " + Bytes.toStringBinary(row));
}
throw new DataSetException("Increment failed on table " + tableId, e);
}
}
private Put getIncrementalPut(byte[] row, Map<byte[], Long> increments) {
Put increment = getIncrementalPut(row);
for (Map.Entry<byte[], Long> column : increments.entrySet()) {
// note: we use default timestamp (current), which is fine because we know we collect metrics no more
// frequent than each second. We also rely on same metric value to be processed by same metric processor
// instance, so no conflicts are possible.
increment.add(columnFamily, column.getKey(), Bytes.toBytes(column.getValue()));
}
return increment;
}
private Put getIncrementalPut(byte[] row) {
return tableUtil.buildPut(row)
.setAttribute(HBaseTable.DELTA_WRITE, Bytes.toBytes(true))
.build();
}
@Override
public void increment(NavigableMap<byte[], NavigableMap<byte[], Long>> updates) {
List<Put> puts = Lists.newArrayList();
for (Map.Entry<byte[], NavigableMap<byte[], Long>> update : updates.entrySet()) {
Put increment = getIncrementalPut(update.getKey(), update.getValue());
puts.add(increment);
}
try {
hTable.put(puts);
hTable.flushCommits();
} catch (IOException e) {
// figure out whether this is an illegal increment
// currently there is not other way to extract that from the HBase exception than string match
if (e.getMessage() != null && e.getMessage().contains("isn't 64 bits wide")) {
throw new NumberFormatException("Attempted to increment a value that is not convertible to long.");
}
throw new DataSetException("Increment failed on table " + tableId, e);
}
}
@Override
public long incrementAndGet(byte[] row, byte[] column, long delta) {
Increment increment = new Increment(row);
increment.addColumn(columnFamily, column, delta);
try {
Result result = hTable.increment(increment);
return Bytes.toLong(result.getValue(columnFamily, column));
} catch (IOException e) {
// figure out whether this is an illegal increment
// currently there is not other way to extract that from the HBase exception than string match
if (e.getMessage() != null && e.getMessage().contains("isn't 64 bits wide")) {
throw new NumberFormatException("Attempted to increment a value that is not convertible to long," +
" row: " + Bytes.toStringBinary(row) +
" column: " + Bytes.toStringBinary(column));
}
throw new DataSetException("IncrementAndGet failed on table " + tableId, e);
}
}
@Override
public void delete(byte[] row, byte[][] columns) {
DeleteBuilder delete = tableUtil.buildDelete(row);
for (byte[] column : columns) {
delete.deleteColumns(columnFamily, column);
}
try {
hTable.delete(delete.build());
} catch (IOException e) {
throw new DataSetException("Delete failed on table " + tableId, e);
}
}
@Override
public Scanner scan(@Nullable byte[] startRow, @Nullable byte[] stopRow,
@Nullable FuzzyRowFilter filter) {
ScanBuilder scanBuilder = tableUtil.buildScan();
configureRangeScan(scanBuilder, startRow, stopRow, filter);
try {
ResultScanner resultScanner = hTable.getScanner(scanBuilder.build());
return new HBaseScanner(resultScanner, columnFamily);
} catch (IOException e) {
throw new DataSetException("Scan failed on table " + tableId, e);
}
}
private ScanBuilder configureRangeScan(ScanBuilder scan, @Nullable byte[] startRow, @Nullable byte[] stopRow,
@Nullable FuzzyRowFilter filter) {
// todo: should be configurable
scan.setCaching(1000);
if (startRow != null) {
scan.setStartRow(startRow);
}
if (stopRow != null) {
scan.setStopRow(stopRow);
}
scan.addFamily(columnFamily);
if (filter != null) {
List<Pair<byte[], byte[]>> fuzzyPairs = Lists.newArrayListWithExpectedSize(filter.getFuzzyKeysData().size());
for (ImmutablePair<byte[], byte[]> pair : filter.getFuzzyKeysData()) {
fuzzyPairs.add(Pair.newPair(pair.getFirst(), pair.getSecond()));
}
scan.setFilter(new org.apache.hadoop.hbase.filter.FuzzyRowFilter(fuzzyPairs));
}
return scan;
}
@Override
public void close() throws IOException {
hTable.close();
}
}