/* * Copyright © 2014 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.transaction.queue.hbase.coprocessor; import co.cask.cdap.common.conf.CConfiguration; import co.cask.cdap.data2.transaction.queue.QueueConstants; import co.cask.cdap.data2.transaction.queue.QueueEntryRow; import co.cask.tephra.Transaction; import co.cask.tephra.TransactionCodec; import co.cask.tephra.TxConstants; import co.cask.tephra.persist.TransactionSnapshot; import co.cask.tephra.persist.TransactionVisibilityState; import co.cask.tephra.util.TxUtils; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Supplier; import com.google.common.base.Throwables; import com.google.common.collect.Maps; import com.google.common.io.InputSupplier; import org.apache.hadoop.hbase.TableName; import org.apache.hadoop.hbase.TableNotFoundException; import org.apache.hadoop.hbase.client.HTableInterface; import org.apache.hadoop.hbase.client.Result; import org.apache.hadoop.hbase.client.ResultScanner; import org.apache.hadoop.hbase.client.Scan; import org.apache.hadoop.hbase.util.Bytes; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; import java.util.NavigableMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import javax.annotation.Nullable; /** * Provides a RegionServer shared cache for all instances of {@code HBaseQueueRegionObserver} of the recent * queue consumer configuration. */ public class ConsumerConfigCache { private static final Logger LOG = LoggerFactory.getLogger(ConsumerConfigCache.class); // Number of bytes for consumer state column (groupId + instanceId) private static final int STATE_COLUMN_SIZE = Bytes.SIZEOF_LONG + Bytes.SIZEOF_INT; // update interval for CConfiguration private static final long CONFIG_UPDATE_FREQUENCY = 300 * 1000L; private static final ConcurrentMap<TableName, ConsumerConfigCache> INSTANCES = new ConcurrentHashMap<>(); private final TableName queueConfigTableName; private final CConfigurationReader cConfReader; private final Supplier<TransactionVisibilityState> transactionSnapshotSupplier; private final InputSupplier<HTableInterface> hTableSupplier; private final TransactionCodec txCodec; private Thread refreshThread; private long lastUpdated; private volatile Map<byte[], QueueConsumerConfig> configCache = Maps.newTreeMap(Bytes.BYTES_COMPARATOR); private long configCacheUpdateFrequency = QueueConstants.DEFAULT_QUEUE_CONFIG_UPDATE_FREQUENCY; private CConfiguration conf; // timestamp of the last update from the configuration table private long lastConfigUpdate; /** * Constructs a new instance. * * @param queueConfigTableName table name that stores queue configuration * @param cConfReader reader to read the latest {@link CConfiguration} * @param transactionSnapshotSupplier A supplier for the latest {@link TransactionSnapshot} * @param hTableSupplier A supplier for creating {@link HTableInterface}. */ ConsumerConfigCache(TableName queueConfigTableName, CConfigurationReader cConfReader, Supplier<TransactionVisibilityState> transactionSnapshotSupplier, InputSupplier<HTableInterface> hTableSupplier) { this.queueConfigTableName = queueConfigTableName; this.cConfReader = cConfReader; this.transactionSnapshotSupplier = transactionSnapshotSupplier; this.hTableSupplier = hTableSupplier; this.txCodec = new TransactionCodec(); } private void init() { startRefreshThread(); } public boolean isAlive() { return refreshThread.isAlive(); } @Nullable public QueueConsumerConfig getConsumerConfig(byte[] queueName) { return configCache.get(queueName); } private void updateConfig() { long now = System.currentTimeMillis(); if (this.conf == null || now > (lastConfigUpdate + CONFIG_UPDATE_FREQUENCY)) { try { this.conf = cConfReader.read(); if (this.conf != null) { LOG.info("Reloaded CConfiguration at {}", now); this.lastConfigUpdate = now; long configUpdateFrequency = conf.getLong(QueueConstants.QUEUE_CONFIG_UPDATE_FREQUENCY, QueueConstants.DEFAULT_QUEUE_CONFIG_UPDATE_FREQUENCY); LOG.info("Will reload consumer config cache every {} seconds", configUpdateFrequency); this.configCacheUpdateFrequency = configUpdateFrequency * 1000; } } catch (IOException ioe) { LOG.error("Error reading default configuration table", ioe); } } } /** * This forces an immediate update of the config cache. It should only be called from the refresh thread or from * tests, to avoid having to add a sleep for the duration of the refresh interval. * * This method is synchronized to protect from race conditions if called directly from a test. Otherwise this is * only called from the refresh thread, and there will not be concurrent invocations. * * @throws IOException if failed to update config cache */ @VisibleForTesting public synchronized void updateCache() throws IOException { Map<byte[], QueueConsumerConfig> newCache = Maps.newTreeMap(Bytes.BYTES_COMPARATOR); long now = System.currentTimeMillis(); TransactionVisibilityState txSnapshot = transactionSnapshotSupplier.get(); if (txSnapshot == null) { LOG.debug("No transaction snapshot is available. Not updating the consumer config cache."); return; } HTableInterface table = hTableSupplier.getInput(); try { // Scan the table with the transaction snapshot Scan scan = new Scan(); scan.addFamily(QueueEntryRow.COLUMN_FAMILY); Transaction tx = TxUtils.createDummyTransaction(txSnapshot); setScanAttribute(scan, TxConstants.TX_OPERATION_ATTRIBUTE_KEY, txCodec.encode(tx)); ResultScanner scanner = table.getScanner(scan); int configCnt = 0; for (Result result : scanner) { if (!result.isEmpty()) { NavigableMap<byte[], byte[]> familyMap = result.getFamilyMap(QueueEntryRow.COLUMN_FAMILY); if (familyMap != null) { configCnt++; Map<ConsumerInstance, byte[]> consumerInstances = new HashMap<>(); // Gather the startRow of all instances across all consumer groups. int numGroups = 0; Long groupId = null; for (Map.Entry<byte[], byte[]> entry : familyMap.entrySet()) { if (entry.getKey().length != STATE_COLUMN_SIZE) { continue; } long gid = Bytes.toLong(entry.getKey()); int instanceId = Bytes.toInt(entry.getKey(), Bytes.SIZEOF_LONG); consumerInstances.put(new ConsumerInstance(gid, instanceId), entry.getValue()); // Columns are sorted by groupId, hence if it change, then numGroups would get +1 if (groupId == null || groupId != gid) { numGroups++; groupId = gid; } } byte[] queueName = result.getRow(); newCache.put(queueName, new QueueConsumerConfig(consumerInstances, numGroups)); } } } long elapsed = System.currentTimeMillis() - now; this.configCache = newCache; this.lastUpdated = now; if (LOG.isDebugEnabled()) { LOG.debug("Updated consumer config cache with {} entries, took {} msec", configCnt, elapsed); } } finally { try { table.close(); } catch (IOException ioe) { LOG.error("Error closing table {}", queueConfigTableName, ioe); } } } private void startRefreshThread() { refreshThread = new Thread("queue-cache-refresh") { @Override public void run() { while (!isInterrupted()) { updateConfig(); long now = System.currentTimeMillis(); if (now > (lastUpdated + configCacheUpdateFrequency)) { try { updateCache(); } catch (TableNotFoundException e) { // This is expected when the namespace goes away since there is one config table per namespace // If the table is not found due to other situation, the region observer already // has logic to get a new one through the getInstance method LOG.warn("Queue config table not found: {}", queueConfigTableName, e); break; } catch (IOException e) { LOG.warn("Error updating queue consumer config cache", e); } } try { Thread.sleep(1000); } catch (InterruptedException ie) { // reset status interrupt(); break; } } LOG.info("Config cache update for {} terminated.", queueConfigTableName); INSTANCES.remove(queueConfigTableName, this); } }; refreshThread.setDaemon(true); refreshThread.start(); } /** * Sets an attribute to the given {@link Scan} object. Instead of calling {@link Scan#setAttribute(String, byte[])} * directly, it uses reflection to call the method. This is because the return type for the setAttribute method * is different in different HBase version. */ private void setScanAttribute(Scan scan, String name, byte[] value) { try { Method setAttribute = scan.getClass().getMethod("setAttribute", String.class, byte[].class); setAttribute.invoke(scan, name, value); } catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException e) { LOG.error("Failed to call Scan.setAttribute", e); throw Throwables.propagate(e); } } public static ConsumerConfigCache getInstance(TableName tableName, CConfigurationReader cConfReader, Supplier<TransactionVisibilityState> txSnapshotSupplier, InputSupplier<HTableInterface> hTableSupplier) { ConsumerConfigCache cache = INSTANCES.get(tableName); if (cache == null) { cache = new ConsumerConfigCache(tableName, cConfReader, txSnapshotSupplier, hTableSupplier); if (INSTANCES.putIfAbsent(tableName, cache) == null) { // if another thread created an instance for the same table, that's ok, we only init the one saved cache.init(); } else { // discard our instance and re-retrieve, someone else set it cache = INSTANCES.get(tableName); } } return cache; } }