/**
* (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.mapreduce;
import java.io.IOException;
import java.util.Collections;
import java.util.Set;
import com.google.common.base.Preconditions;
import com.google.common.collect.Sets;
import org.apache.commons.io.IOUtils;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.kiji.annotations.ApiAudience;
import org.kiji.annotations.ApiStability;
import org.kiji.mapreduce.framework.JobHistoryKijiTable;
import org.kiji.mapreduce.framework.KijiConfKeys;
import org.kiji.schema.Kiji;
import org.kiji.schema.KijiURI;
/** A runnable MapReduce job that interacts with Kiji tables. */
@ApiAudience.Public
@ApiStability.Stable
public final class KijiMapReduceJob {
private static final Logger LOG = LoggerFactory.getLogger(KijiMapReduceJob.class);
/** The wrapped Hadoop Job. */
private final Job mJob;
// TODO(KIJIMR-92): Versions of Hadoop after 20.x add the ability to get start and
// end times directly from a Job, making these superfluous.
/** Used to track when the job's execution begins. */
private long mJobStartTime;
/** Used to track when the job's execution ends. */
private long mJobEndTime;
/** Completion polling thread. */
private Thread mCompletionPollingThread;
/** Whether the job is currently started. */
private boolean mJobStarted = false;
/**
* Creates a new <code>KijiMapReduceJob</code> instance.
*
* @param job The Hadoop job to run.
*/
private KijiMapReduceJob(Job job) {
mJob = Preconditions.checkNotNull(job);
}
/**
* Creates a new <code>KijiMapReduceJob</code>.
*
* @param job is a Hadoop {@link Job} that interacts with Kiji and will be wrapped by the new
* <code>KijiMapReduceJob</code>.
* @return A new <code>KijiMapReduceJob</code> backed by a Hadoop {@link Job}.
*/
public static KijiMapReduceJob create(Job job) {
return new KijiMapReduceJob(job);
}
/**
* Join the completion polling thread and block until it exits.
*
* @throws InterruptedException if the completion polling thread is interrupted.
* @throws IOException in case of an IO error when querying job success.
* @return Whether the job completed successfully
*/
public boolean join() throws InterruptedException, IOException {
Preconditions.checkState(mJobStarted,
"Cannot join completion polling thread because the job is not running.");
mCompletionPollingThread.join();
return mJob.isSuccessful();
}
/**
* The status of a job that was started asynchronously using {@link #submit()}.
*/
public static final class Status {
/** The Job whose status is being tracked. */
private final Job mJob;
/**
* Constructs a <code>Status</code> around a Hadoop job.
*
* @param job The Hadoop job being run.
*/
protected Status(Job job) {
mJob = job;
}
/**
* Determines whether the job has completed.
*
* @return Whether the job has completed.
* @throws IOException If there is an error querying the job.
*/
public boolean isComplete() throws IOException {
return mJob.isComplete();
}
/**
* Determines whether the job was successful. The return value is undefined if the
* job has not yet completed.
*
* @return Whether the job was successful.
* @throws IOException If there is an error querying the job.
*/
public boolean isSuccessful() throws IOException {
return mJob.isSuccessful();
}
}
/**
* Records information about a completed job into the history table of a Kiji instance.
*
* If the attempt fails due to an IOException (a Kiji cannot be made, there is no job history
* table, its content is corrupted, etc.), we catch the exception and warn the user.
*
* @param kiji Kiji instance to write the job record to.
* @throws IOException on I/O error.
*/
private void recordJobHistory(Kiji kiji) throws IOException {
final Job job = getHadoopJob();
JobHistoryKijiTable jobHistory = null;
try {
jobHistory = JobHistoryKijiTable.open(kiji);
jobHistory.recordJob(job, mJobStartTime, mJobEndTime);
} catch (IOException ioe) {
// We swallow errors for recording jobs, because it's a non-fatal error for the task.
LOG.warn(
"Error recording job {} in history table of Kiji instance {}:\n"
+ "{}\n"
+ "This does not affect the success of job {}.\n",
getHadoopJob().getJobID(), kiji.getURI(),
StringUtils.stringifyException(ioe),
getHadoopJob().getJobID());
} finally {
IOUtils.closeQuietly(jobHistory);
}
}
/**
* Records information about a completed job into all relevant Kiji instances.
*
* Underlying failures are logged but not fatal.
*/
private void recordJobHistory() {
final Configuration conf = getHadoopJob().getConfiguration();
final Set<KijiURI> instanceURIs = Sets.newHashSet();
if (conf.get(KijiConfKeys.KIJI_INPUT_TABLE_URI) != null) {
instanceURIs.add(KijiURI.newBuilder(conf.get(KijiConfKeys.KIJI_INPUT_TABLE_URI))
.withTableName(null)
.withColumnNames(Collections.<String>emptyList())
.build());
}
if (conf.get(KijiConfKeys.KIJI_OUTPUT_TABLE_URI) != null) {
instanceURIs.add(KijiURI.newBuilder(conf.get(KijiConfKeys.KIJI_OUTPUT_TABLE_URI))
.withTableName(null)
.withColumnNames(Collections.<String>emptyList())
.build());
}
for (KijiURI instanceURI : instanceURIs) {
if (instanceURI != null) {
try {
final Kiji kiji = Kiji.Factory.open(instanceURI, conf);
try {
recordJobHistory(kiji);
} finally {
kiji.release();
}
} catch (IOException ioe) {
LOG.warn(
"Error recording job {} in history table of Kiji instance {}: {}\n"
+ "This does not affect the success of job {}.",
getHadoopJob().getJobID(),
instanceURI,
ioe.getMessage(),
getHadoopJob().getJobID());
}
}
}
}
/**
* Gives access the underlying Hadoop Job object.
*
* @return The Hadoop Job object.
*/
public Job getHadoopJob() {
return mJob;
}
// Unfortunately, our use of an anonymous inner class in this method confuses checkstyle.
// We disable it temporarily.
// CSOFF: VisibilityModifierCheck
/**
* Starts the job and return immediately.
*
* @return The job status. This can be queried for completion and success or failure.
* @throws ClassNotFoundException If a required class cannot be found on the classpath.
* @throws IOException If there is an IO error.
* @throws InterruptedException If the thread is interrupted.
*/
public Status submit() throws ClassNotFoundException, IOException, InterruptedException {
mJobStarted = true;
mJobStartTime = System.currentTimeMillis();
LOG.debug("Submitting job");
mJob.submit();
final Status jobStatus = new Status(mJob);
// We use an inline defined thread here to poll jobStatus for completion to update
// our job history table.
mCompletionPollingThread = new Thread() {
// Polling interval in milliseconds.
private final int mPollInterval =
mJob.getConfiguration().getInt(KijiConfKeys.KIJI_MAPREDUCE_POLL_INTERVAL, 1000);
public void run() {
try {
while (!jobStatus.isComplete()) {
Thread.sleep(mPollInterval);
}
mJobEndTime = System.currentTimeMillis();
// IOException in recordJobHistory() are caught and logged.
recordJobHistory();
} catch (IOException e) {
// If we catch an error while polling, bail out.
LOG.debug("Error polling jobStatus.");
return;
} catch (InterruptedException e) {
LOG.debug("Interrupted while polling jobStatus.");
return;
}
}
};
mCompletionPollingThread.setDaemon(true);
mCompletionPollingThread.start();
return jobStatus;
}
// CSON: VisibilityModifierCheck
/**
* Runs the job (blocks until it is complete).
*
* @return Whether the job was successful.
* @throws ClassNotFoundException If a required class cannot be found on the classpath.
* @throws IOException If there is an IO error.
* @throws InterruptedException If the thread is interrupted.
*/
public boolean run() throws ClassNotFoundException, IOException, InterruptedException {
mJobStartTime = System.currentTimeMillis();
LOG.debug("Running job");
boolean ret = mJob.waitForCompletion(true);
mJobEndTime = System.currentTimeMillis();
try {
recordJobHistory();
} catch (Throwable thr) {
thr.printStackTrace();
}
return ret;
}
}