/* * 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.data.hbase; import com.google.common.base.Function; import com.google.common.base.Throwables; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.hbase.MiniHBaseCluster; import org.apache.hadoop.hbase.TableName; import org.apache.hadoop.hbase.client.HBaseAdmin; import org.apache.hadoop.hbase.regionserver.HRegion; import org.apache.hadoop.hbase.util.Bytes; import org.apache.hadoop.hbase.util.JVMClusterUtil; import org.junit.rules.ExternalResource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; /** * A base class that can be used to easily run a test within an embedded * HBase cluster (includes embedded ZooKeeper, HDFS, and HBase). * * To use, simply extend this class and create your tests like normal. From * within your tests, you can access the underlying HBase cluster through * {@link #getConfiguration()}, {@link #getHBaseAdmin()} * Alternatively, you can call the {@link #startHBase()} and {@link #stopHBase()} * methods directly from within your own BeforeClass/AfterClass methods. * * Note: This test is somewhat heavy-weight and takes 10-20 seconds to startup. */ public abstract class HBaseTestBase extends ExternalResource { private static final Logger LOG = LoggerFactory.getLogger(HBaseTestBase.class); // Accessors for test implementations public abstract Configuration getConfiguration(); public HBaseAdmin getHBaseAdmin() throws IOException { return new HBaseAdmin(getConfiguration()); } public String getZkConnectionString() { return "localhost:" + getZKClientPort(); } protected abstract int getZKClientPort(); // Test startup / teardown private void startHBase() throws Exception { // Tune down the connection thread pool size getConfiguration().setInt("hbase.hconnection.threads.core", 5); getConfiguration().setInt("hbase.hconnection.threads.max", 10); // Tunn down handler threads in regionserver getConfiguration().setInt("hbase.regionserver.handler.count", 10); // Set to random port getConfiguration().setInt("hbase.master.port", 0); getConfiguration().setInt("hbase.master.info.port", 0); getConfiguration().setInt("hbase.regionserver.port", 0); getConfiguration().setInt("hbase.regionserver.info.port", 0); doStartHBase(); } protected abstract void doStartHBase() throws Exception; protected abstract void stopHBase() throws Exception; // HRegion-level testing public abstract HRegion createHRegion(byte[] tableName, byte[] startKey, byte[] stopKey, String callingMethod, Configuration conf, byte[]... families) throws IOException; /** * Force and block on a flush to occur on all regions of table {@code tableName}. * @param tableName The table whose regions should be flushed. */ public void forceRegionFlush(byte[] tableName) throws IOException { MiniHBaseCluster hbaseCluster = getHBaseCluster(); if (hbaseCluster != null) { TableName qualifiedTableName = TableName.valueOf(tableName); for (JVMClusterUtil.RegionServerThread t : hbaseCluster.getRegionServerThreads()) { List<HRegion> serverRegions = t.getRegionServer().getOnlineRegions(qualifiedTableName); List<Runnable> flushers = new ArrayList<>(); for (HRegion region : serverRegions) { flushers.add(createFlushRegion(region)); } parallelRun(flushers); LOG.info("RegionServer {}: Flushed {} regions for table {}", t.getRegionServer().getServerName().toString(), serverRegions.size(), Bytes.toStringBinary(tableName)); } } } /** * Force and block on a compaction on all regions of table {@code tableName}. * @param tableName The table whose regions should be compacted. * @param majorCompact Whether a major compaction should be requested. */ public void forceRegionCompact(byte[] tableName, boolean majorCompact) throws IOException { MiniHBaseCluster hbaseCluster = getHBaseCluster(); if (hbaseCluster != null) { TableName qualifiedTableName = TableName.valueOf(tableName); for (JVMClusterUtil.RegionServerThread t : hbaseCluster.getRegionServerThreads()) { List<HRegion> serverRegions = t.getRegionServer().getOnlineRegions(qualifiedTableName); List<Runnable> compacters = new ArrayList<>(); for (HRegion region : serverRegions) { compacters.add(createCompactRegion(region, majorCompact)); } parallelRun(compacters); LOG.info("RegionServer {}: Compacted {} regions for table {}", t.getRegionServer().getServerName().toString(), serverRegions.size(), Bytes.toStringBinary(tableName)); } } } /** * Creates a {@link Runnable} that flushes the given HRegion when run. */ public Runnable createFlushRegion(final HRegion region) { return new Runnable() { @Override public void run() { try { region.flushcache(); } catch (IOException e) { throw Throwables.propagate(e); } } }; } /** * Creates a {@link Runnable} that compacts the given HRegion when run. */ public Runnable createCompactRegion(final HRegion region, final boolean majorCompact) { return new Runnable() { @Override public void run() { try { region.compactStores(majorCompact); } catch (IOException e) { throw Throwables.propagate(e); } } }; } /** * Applies a {@link Function} on each HRegion for a given table, and returns a map of the results, keyed * by region name. * @param tableName The table whose regions should be processed. * @param function The function to apply on each region. * @param <T> The return type for the function. * @return a map of region name to the result of applying the specified function. */ public abstract <T> Map<byte[], T> forEachRegion(byte[] tableName, Function<HRegion, T> function); public abstract MiniHBaseCluster getHBaseCluster(); public abstract void waitUntilTableAvailable(byte[] tableName, long timeoutInMillis) throws IOException, InterruptedException; /** * Executes the given list of Runnable in parallel using a fixed thread pool executor. This method blocks * until all runnables finished. */ private void parallelRun(List<? extends Runnable> runnables) { ListeningExecutorService executor = MoreExecutors.listeningDecorator( Executors.newFixedThreadPool(runnables.size())); try { List<ListenableFuture<?>> futures = new ArrayList<>(runnables.size()); for (Runnable r : runnables) { futures.add(executor.submit(r)); } Futures.getUnchecked(Futures.allAsList(futures)); } finally { executor.shutdownNow(); try { executor.awaitTermination(60, TimeUnit.SECONDS); } catch (InterruptedException e) { LOG.error("Interrupted", e); } } } @Override protected void before() throws Throwable { startHBase(); } @Override protected void after() { try { stopHBase(); } catch (Exception e) { LOG.error("Error stopping HBase", e); } } }