Package org.kiji.schema.testutil

Source Code of org.kiji.schema.testutil.AbstractKijiIntegrationTest$KijiDeletionThread

/**
* (c) Copyright 2012 WibiData, Inc.
*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* 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 org.kiji.schema.testutil;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.URL;
import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.Semaphore;

import com.google.common.base.Preconditions;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.HBaseTestingUtility;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.rules.TemporaryFolder;
import org.junit.rules.TestName;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.kiji.schema.KijiInstaller;
import org.kiji.schema.KijiURI;
import org.kiji.schema.platform.SchemaPlatformBridge;
import org.kiji.schema.tools.BaseTool;
import org.kiji.schema.util.Debug;
import org.kiji.schema.util.ResourceUtils;

/**
* A base class for all Kiji integration tests.
*
* <p>
*   This class sets up a Kiji instance before each test and tears it down afterwards.
*   It assumes there is an HBase cluster running already, and its configuration is on the
*   classpath.
* </p>
*
* <p>
*   To avoid stepping on other Kiji instances, the name of the instance created is
*   a random unique identifier.
* </p>
*
* This class is abstract because it has a lot of boilerplate for setting up integration
* tests but doesn't actually test anything.
*
* The STANDALONE variable controls whether the test creates an embedded HBase and M/R mini-cluster
* for itself. This allows run a single test in a debugger without external setup.
*/
public abstract class AbstractKijiIntegrationTest {
  private static final Logger LOG = LoggerFactory.getLogger(AbstractKijiIntegrationTest.class);

  public static final String ADD_CLASSPATH_TO_JOB_DCACHE_PROPERTY =
      "org.kiji.mapreduce.add.classpath.to.job.dcache";

  static {
    SchemaPlatformBridge.get().initializeHadoopResources();
    if (System.getProperty(ADD_CLASSPATH_TO_JOB_DCACHE_PROPERTY, null) == null) {
      System.setProperty(ADD_CLASSPATH_TO_JOB_DCACHE_PROPERTY, "true");
    }
  }

  /**
   * Name of the property to specify an external HBase instance.
   *
   * From Maven, one may specify an external HBase cluster with:
   *   mvn clean verify -Dkiji.test.cluster.uri=kiji://localhost:2181
   */
  private static final String BASE_TEST_URI_PROPERTY = "kiji.test.cluster.uri";

  public static final String TEST_CLUSTER_URI = System.getProperty(BASE_TEST_URI_PROPERTY);

  public static final String CLEANUP_AFTER_TEST_PROPERTY =
      "org.kiji.schema.test.cleanup.after.test";

  /**
   * Whether to remove temporary kiji instances after testing is over. This defaults to true
   * if an external HBase instance is specified, or false otherwise.
   */
  public static final boolean CLEANUP_AFTER_TEST = Boolean.parseBoolean(System.getProperty(
      CLEANUP_AFTER_TEST_PROPERTY, (TEST_CLUSTER_URI == null ? "false" : "true")));

  /* Semaphore for tracking how many tests are currently running. */
  private static final Semaphore RUNNING_TEST_SEMAPHORE = new Semaphore(0);

  /** An integration helper for installing and removing instances. */
  private IntegrationHelper mHelper;

  /** The URI to be used for running tests on the hbase-maven-plugin. */
  private static final String HBASE_MAVEN_PLUGIN_URI = "kiji://.env/";

  private static KijiURI mHBaseURI;

  // -----------------------------------------------------------------------------------------------
  // Stand-alone integration test setup

  private static HBaseTestingUtility mHBaseUtil;

  /** Configuration created by the standalone setup, if enabled. */
  private static Configuration mStandaloneConf = null;

  // -----------------------------------------------------------------------------------------------

  /** Test configuration. */
  private Configuration mConf;

  /** The randomly generated URI for the instance. */
  private KijiURI mKijiURI;

  // JUnit requires public, checkstyle disagrees:
  // CSOFF: VisibilityModifierCheck
  /** Test method name (eg. "testFeatureX"). */
  @Rule
  public final TestName mTestName = new TestName();

  /** A temporary directory for test data. */
  @Rule
  public TemporaryFolder mTempDir = new TemporaryFolder();
  // CSON: VisibilityModifierCheck

  // mCreationThread and mDeletionThread are lazily initialized in a @BeforeClass
  // handler. This ensures that only one such @Before method does the work.
  private static final Object THREAD_MANAGEMENT_LOCK = new Object();

  /** A thread that creates Kiji instances for use by tests. */
  private static KijiCreationThread mCreationThread;
  /** A thread that removes Kiji instances no longer in use by tests. */
  private static KijiDeletionThread mDeletionThread;

  /**
   * Creates a Configuration object for the HBase instance to use.
   *
   * @return an HBase configuration to work against.
   */
  protected Configuration createConfiguration() {
    final Configuration conf = (null != mStandaloneConf)
        ? HBaseConfiguration.create(mStandaloneConf)
        : HBaseConfiguration.create();

    // Set the mapred.output.dir to a unique temporary directory for each integration test:
    final String tempDir = conf.get("hadoop.tmp.dir");
    Preconditions.checkNotNull(
        tempDir,
        "hadoop.tmp.dir must be set in the configuration for integration tests to use.");
    conf.set("mapred.output.dir", new Path(tempDir, UUID.randomUUID().toString()).toString());

    return conf;
  }

  /**
   * Determine the URI for the HBase instance to use.
   *
   * @return the URI for the HBase instance to use.
   */
  protected static KijiURI getHBaseURI() {
    if (TEST_CLUSTER_URI != null) {
      return KijiURI.newBuilder(TEST_CLUSTER_URI).build();
    } else {
      return KijiURI.newBuilder(HBASE_MAVEN_PLUGIN_URI).build();
    }
  }

  @BeforeClass
  public static void setupManagementThreads() throws Exception {
    mHBaseURI = getHBaseURI();

    synchronized (THREAD_MANAGEMENT_LOCK) {
      // Create background worker threads if they're not already created.
      if (null == mCreationThread) {
        LOG.info("Starting Kiji instance creation thread.");
        mCreationThread = new KijiCreationThread(mHBaseURI);
        mCreationThread.start();
        if (CLEANUP_AFTER_TEST) {
          LOG.info("Starting Kiji instance deletion thread.");
          mDeletionThread = new KijiDeletionThread();
          mDeletionThread.start();
        }
      }
      RUNNING_TEST_SEMAPHORE.release();
    }
  }

  @AfterClass
  public static void teardownManagementThreads() {
    synchronized (THREAD_MANAGEMENT_LOCK) {
      // Should always have a permit available since each thread offers one
      RUNNING_TEST_SEMAPHORE.tryAcquire();

      // Last one out shuts off the lights.
      if (RUNNING_TEST_SEMAPHORE.availablePermits() == 0) {
        // Put the remaining created instances into the deletion queue
        mCreationThread.stopKijiCreation();
        KijiURI unusedKijiURI = mCreationThread.getKijiForCleanup();
        while (CLEANUP_AFTER_TEST && (null != unusedKijiURI)) {
          try {
            mDeletionThread.destroyKiji(unusedKijiURI);
          } catch (InterruptedException exn) {
            LOG.error("Failed to put Kiji instance '{}' into the deletion queue: {}",
                unusedKijiURI, exn.getMessage());
            continue;
          }
          unusedKijiURI = mCreationThread.getKijiForCleanup();
        }

        // Finish deleting and clear the threads
        if (CLEANUP_AFTER_TEST) {
          mDeletionThread.waitForCompletion();
        }
        mCreationThread = null;
        mDeletionThread = null;
      }
    }
  }

  @Before
  public final void setupKijiIntegrationTest() throws Exception {
    mConf = createConfiguration();

    LOG.info("Setup summary for {}", getClass().getName());
    Debug.logConfiguration(mConf);

    // Get a new Kiji instance, with a randomly-generated name.
    mKijiURI = mCreationThread.getFreshKiji();
    mHelper = new IntegrationHelper(mConf);
  }

  @After
  public final void teardownKijiIntegrationTest() throws Exception {
    // Schedule the Kiji instance for asynchronous deletion.
    if (CLEANUP_AFTER_TEST && (null != mKijiURI)) {
      mDeletionThread.destroyKiji(mKijiURI);
    }

    mHelper = null;
    mKijiURI = null;
    mConf = null;
  }

  /** @return The integration helper. */
  protected IntegrationHelper getIntegrationHelper() {
    return mHelper;
  }

  /** @return The KijiURI for this test instance. */
  protected KijiURI getKijiURI() {
    return mKijiURI;
  }

  /** @return The name of the instance installed for this test. */
  protected String getInstanceName() {
    return mKijiURI.getInstance();
  }

  /** @return The temporary directory to use for test data. */
  protected File getTempDir() {
    return mTempDir.getRoot();
  }

  /** @return a test Configuration, with a MapReduce and HDFS cluster. */
  public Configuration getConf() {
    return mConf;
  }

  /**
   * Gets the path to a file in the mini HDFS cluster.
   *
   * <p>If the file does not yet exist, it will be copied into the cluster first.</p>
   *
   * @param localResource A local file resource.
   * @return The path to the file in the mini HDFS filesystem.
   * @throws java.io.IOException If there is an error.
   */
  protected Path getDfsPath(URL localResource) throws IOException {
    return getDfsPath(localResource.getPath());
  }


  /**
   * Gets the path to a file in the mini HDFS cluster.
   *
   * <p>If the file does not yet exist, it will be copied into the cluster first.</p>
   *
   * @param localPath A local file path (doesn't have to exist, but if it does it will be copied).
   * @return The path to the file in the mini HDFS filesystem.
   * @throws java.io.IOException If there is an error.
   */
  protected Path getDfsPath(String localPath) throws IOException {
    return getDfsPath(new File(localPath));
  }

  /**
   * Gets the path to a file in the mini HDFS cluster.
   *
   * <p>If the file does not yet exist, it will be copied into the cluster first.</p>
   *
   * @param localFile A local file.
   * @return The path to the file in the mini HDFS filesystem.
   * @throws java.io.IOException If there is an error.
   */
  protected Path getDfsPath(File localFile) throws IOException {
    String uniquePath
        = new File(getClass().getName().replaceAll("\\.\\$", "/"), localFile.getPath()).getPath();
    if (localFile.exists()) {
      return mHelper.copyToDfs(localFile, uniquePath);
    }
    return mHelper.getDfsPath(uniquePath);
  }

  /**
   * Runs a tool within the instance for this test and captures the console output.
   *
   * @param tool The tool to run.
   * @param args The command-line args to pass to the tool.  The --instance flag will be added.
   * @return A result with the captured tool output.
   * @throws Exception If there is an error.
   */
  protected ToolResult runTool(BaseTool tool, String[] args) throws Exception {
    // Append the --instance=<instance-name> flag on the end of the args.
    final String[] argsWithKiji = Arrays.copyOf(args, args.length + 1);
    argsWithKiji[args.length] = "--debug=true";
    return mHelper.runTool(mHelper.getConf(), tool, argsWithKiji);
  }

  /**
   * Creates and populates a test table of users called 'foo'.
   *
   * @throws Exception If there is an error.
   */
  protected void createAndPopulateFooTable() throws Exception {
    mHelper.createAndPopulateFooTable(mKijiURI);
  }

  /**
   * Deletes the table created with createAndPopulateFooTable().
   *
   * @throws Exception If there is an error.
   */
  public void deleteFooTable() throws Exception {
    mHelper.deleteFooTable(mKijiURI);
  }

  /**
   * Formats an exception stack trace into a string.
   *
   * @param exn Exception to format.
   * @return the exception stack trace, as a string.
   */
  protected static String formatException(Exception exn) {
    final StringWriter writer = new StringWriter();
    final PrintWriter printWriter = new PrintWriter(writer);
    exn.printStackTrace(printWriter);
    ResourceUtils.closeOrLog(printWriter);
    return writer.toString();
  }

  /**
   * Thread that creates Kiji instances for use by integration tests.
   */
  private static final class KijiCreationThread extends Thread {
    /** Maintain a pool of this many fresh Kiji instances ready to go. */
    private static final int INSTANCE_POOL_SIZE = 4;

    /** Ensures that this thread is either in running mode or cleanup mode. */
    private static final Object THREAD_CREATION_ACTIVE_LOCK = new Object();

    /** Bounded producer/consumer queue that holds the names of new Kiji instances. */
    private final LinkedBlockingQueue<KijiURI> mKijiQueue =
        new LinkedBlockingQueue<KijiURI>(INSTANCE_POOL_SIZE);

    private final KijiURI mHBaseURI;

    /** Denotes whether this thread should continue to create Kiji instances. */
    private boolean mActive = true;


    private KijiCreationThread(KijiURI hbaseURI) {
      setName("KijiCreationThread");
      setDaemon(true);
      mHBaseURI = hbaseURI;
    }

    /**
     * Get a new Kiji instance from the pool. This may block if we're using Kiji
     * instances in tests too fast.
     *
     * @return a KijiURI identifying an unused Kiji instance.
     * @throws InterruptedException if the blocking call is interrupted.
     */
    public KijiURI getFreshKiji() throws InterruptedException {
      return mKijiQueue.take();
    }

    /**
     * Get a remaining Kiji instance from the pool for cleaning up. This may block if we're out
     * of Kiji instances and there are still instances being created
     *
     * Package private as this should only be use for cleanup
     *
     * @return a KijiURI identifying an unused Kiji instance for cleaning up
     */
    KijiURI getKijiForCleanup() {
      if (!mKijiQueue.isEmpty()) {
        return mKijiQueue.poll();
      }
      synchronized (THREAD_CREATION_ACTIVE_LOCK) {
        return mKijiQueue.poll();
      }
    }

    /**
     * Stops the creation of new Kiji instances.  The creation thread will finish creating any
     * instances that are still in progress.
     */
    public void stopKijiCreation() {
      mActive = false;
    }

    /** {@inheritDoc} */
    @Override
    public void run() {
      synchronized (THREAD_CREATION_ACTIVE_LOCK) {
        while (mActive) {
          // Create a new Kiji instance.
          final String instanceName = UUID.randomUUID().toString().replaceAll("-", "_");
          final Configuration conf = HBaseConfiguration.create();
          try {
            final KijiURI kijiURI =
                KijiURI.newBuilder(mHBaseURI).withInstanceName(instanceName).build();
            KijiInstaller.get().install(kijiURI, conf);

            // This blocks if the queue is full:
            mKijiQueue.put(kijiURI);

          } catch (Exception exn) {
            LOG.error(String.format("Exception while installing Kiji instance [%s]: %s\n%s",
                instanceName, exn.toString(), formatException(exn)));
          }
        }
      } // THREAD_CREATION_ACTIVE_LOCK
    }
  }

  /**
   * Thread that destroys Kiji instances used by integration tests.
   */
  private static final class KijiDeletionThread extends Thread {
    /** Unbounded producer/consumer queue that holds Kiji instances to destroy. */
    private LinkedBlockingQueue<KijiURI> mKijiQueue;

    private boolean mActive;

    /** Ensures that this thread is either in running mode or cleanup mode. */
    private static final Object THREAD_DELETION_ACTIVE_LOCK = new Object();

    private KijiDeletionThread() {
      setName("KijiDeletionThread");
      setDaemon(true);
      mKijiQueue = new LinkedBlockingQueue<KijiURI>();
      mActive = true;
    }

    /**
     * Add a Kiji instance to the list of Kiji instances to be destroyed.
     * The instance will be destroyed asynchronously.
     *
     * @param kijiURI a String identifying a Kiji instance we're done with.
     * @throws InterruptedException if the blocking call is interrupted.
     */
    public void destroyKiji(KijiURI kijiURI) throws InterruptedException {
      mKijiQueue.put(kijiURI);
    }

    /**
     * Tells the deletion thread to stop running and waits until all remaining instances have
     * been uninstalled.
     */
    public void waitForCompletion() {
      mActive = false;
      synchronized (THREAD_DELETION_ACTIVE_LOCK) {
        return;
      }
    }

    /** {@inheritDoc} */
    @Override
    public void run() {
      IntegrationHelper intHelper = new IntegrationHelper(HBaseConfiguration.create());
      synchronized (THREAD_DELETION_ACTIVE_LOCK) {
        while (!mKijiQueue.isEmpty() || mActive) {
          KijiURI instanceURI = null;

          try {
            instanceURI = mKijiQueue.take();
          } catch (InterruptedException ie) {
            // Interruption in here is expected; as long as the queue is non-empty,
            // keep trying to remove one.
            continue;
          }

          if (null == instanceURI) {
            LOG.warn("Unexpected null Kiji instance after take() in destroy thread");
            continue;
          }

          try {
            intHelper.uninstallKiji(instanceURI);
          } catch (Exception e) {
            LOG.error("Could not destroy Kiji instance [" + instanceURI.toString() + "]: "
                + e.getMessage());
          }
        }
      }
    }
  }

}
TOP

Related Classes of org.kiji.schema.testutil.AbstractKijiIntegrationTest$KijiDeletionThread

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.