/*
* Copyright © 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.increment.hbase;
import co.cask.cdap.data2.transaction.coprocessor.DefaultTransactionStateCacheSupplier;
import co.cask.cdap.data2.util.hbase.HTableNameConverter;
import co.cask.tephra.TxConstants;
import co.cask.tephra.coprocessor.TransactionStateCache;
import co.cask.tephra.persist.TransactionVisibilityState;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Supplier;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.HColumnDescriptor;
import org.apache.hadoop.hbase.HTableDescriptor;
import org.apache.hadoop.hbase.util.Bytes;
import java.util.Map;
import java.util.Set;
/**
* Common state and utilities shared by the HBase version-specific {@code IncrementHandler} coprocessor
* implementations. This common implementation cannot go into a shared base class, as each coprocessor needs
* to derive from the HBase version's {@code BaseRegionObserver} class, in order to avoid being broken by
* API changes.
*/
public class IncrementHandlerState {
/**
* Property set for {@link HColumnDescriptor} to indicate if increment is transactional. Default: "true", i.e.
* transactional.
*/
public static final String PROPERTY_TRANSACTIONAL = "dataset.table.readless.increment.transactional";
public static final long MAX_TS_PER_MS = 1000000;
// prefix bytes used to mark values that are deltas vs. full sums
public static final byte[] DELTA_MAGIC_PREFIX = new byte[] { 'X', 'D' };
// expected length for values storing deltas (prefix + increment value)
public static final int DELTA_FULL_LENGTH = DELTA_MAGIC_PREFIX.length + Bytes.SIZEOF_LONG;
public static final int BATCH_UNLIMITED = -1;
public static final Log LOG = LogFactory.getLog(IncrementHandlerState.class);
private final HTableDescriptor hTableDescriptor;
private final HTableNameConverter hTableNameConverter;
private TransactionStateCache cache;
private TimestampOracle timeOracle = new TimestampOracle();
private final Configuration conf;
protected final Set<byte[]> txnlFamilies = Sets.newTreeSet(Bytes.BYTES_COMPARATOR);
protected Map<byte[], Long> ttlByFamily = Maps.newTreeMap(Bytes.BYTES_COMPARATOR);
public IncrementHandlerState(Configuration conf, HTableDescriptor hTableDescriptor,
HTableNameConverter hTableNameConverter) {
this.conf = conf;
this.hTableDescriptor = hTableDescriptor;
this.hTableNameConverter = hTableNameConverter;
}
@VisibleForTesting
public void setTimestampOracle(TimestampOracle timeOracle) {
this.timeOracle = timeOracle;
}
protected Supplier<TransactionStateCache> getTransactionStateCacheSupplier(HTableDescriptor htd, Configuration conf) {
String sysConfigTablePrefix = hTableNameConverter.getSysConfigTablePrefix(htd);
return new DefaultTransactionStateCacheSupplier(sysConfigTablePrefix, conf);
}
public void initFamily(byte[] familyName, Map<byte[], byte[]> familyValues) {
String familyAsString = Bytes.toString(familyName);
byte[] transactionalConfig = familyValues.get(Bytes.toBytes(IncrementHandlerState.PROPERTY_TRANSACTIONAL));
boolean txnl = transactionalConfig == null || !"false".equals(Bytes.toString(transactionalConfig));
LOG.info("Family " + familyAsString + " is transactional: " + txnl);
if (txnl) {
txnlFamilies.add(familyName);
}
// check for TTL configuration
byte[] columnTTL = familyValues.get(Bytes.toBytes(TxConstants.PROPERTY_TTL));
long ttl = 0;
if (columnTTL != null) {
try {
String stringTTL = Bytes.toString(columnTTL);
ttl = Long.parseLong(stringTTL);
LOG.info("Family " + familyAsString + " has TTL of " + ttl);
} catch (NumberFormatException nfe) {
LOG.warn("Invalid TTL value configured for column family " + familyAsString +
", value = " + Bytes.toStringBinary(columnTTL));
}
}
ttlByFamily.put(familyName, ttl);
// get the transaction state cache as soon as we have a transactional family
if (!txnlFamilies.isEmpty() && cache == null) {
Supplier<TransactionStateCache> cacheSupplier = getTransactionStateCacheSupplier(hTableDescriptor, conf);
this.cache = cacheSupplier.get();
}
}
public boolean containsTransactionalFamily(Set<byte[]> familyNames) {
// we assume that if any of the column families written to are transactional, the entire write is transactional
for (byte[] key : familyNames) {
if (txnlFamilies.contains(key)) {
return true;
}
}
return false;
}
/**
* Returns a unique timestamp for the current operation.
*
* @return a new timestamp guaranteed to be unique within the scope of usage
*/
public synchronized long getUniqueTimestamp() {
return timeOracle.getUniqueTimestamp();
}
/**
* Returns the upper bound beyond which we can compact any increment deltas into a new sum.
* @param columnFamily the column family name
* @return the newest timestamp beyond which can compact delta increments
*/
public long getCompactionBound(byte[] columnFamily) {
if (txnlFamilies.contains(columnFamily)) {
TransactionVisibilityState snapshot = cache.getLatestState();
// if tx snapshot is not available, used "0" as upper bound to avoid trashing in-progress tx
return snapshot != null ? snapshot.getVisibilityUpperBound() : 0;
} else {
return Long.MAX_VALUE;
}
}
/**
* Returns the time-to-live (in milliseconds) for the given column family, transformed into the same precision
* used in assigning unique timestamps.
*
* @param familyName the column family name
* @return the time-to-live value
*/
public long getFamilyTTL(byte[] familyName) {
Long configuredTTL = ttlByFamily.get(familyName);
return configuredTTL == null ? -1 : configuredTTL * TxConstants.MAX_TX_PER_MS;
}
/**
* Returns the oldest timestamp that will be visible for a given column family, after the column family's
* configured time-to-live is applied.
* @param familyName the name of the column family
* @return the oldest timestamp value that will be visible
*/
public long getOldestVisibleTimestamp(byte[] familyName) {
long familyTTL = getFamilyTTL(familyName);
return familyTTL > 0 ? timeOracle.currentTime() - familyTTL : 0;
}
}