co.cask.cdap.internal.app.runtime.batch.MapReduceRuntimeService.java Source code

Java tutorial

Introduction

Here is the source code for co.cask.cdap.internal.app.runtime.batch.MapReduceRuntimeService.java

Source

/*
 * Copyright  2014-2016 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.internal.app.runtime.batch;

import co.cask.cdap.api.Resources;
import co.cask.cdap.api.data.batch.DatasetOutputCommitter;
import co.cask.cdap.api.data.batch.InputFormatProvider;
import co.cask.cdap.api.data.batch.OutputFormatProvider;
import co.cask.cdap.api.flow.flowlet.StreamEvent;
import co.cask.cdap.api.mapreduce.MapReduce;
import co.cask.cdap.api.mapreduce.MapReduceContext;
import co.cask.cdap.api.mapreduce.MapReduceSpecification;
import co.cask.cdap.api.stream.StreamEventDecoder;
import co.cask.cdap.common.conf.CConfiguration;
import co.cask.cdap.common.conf.ConfigurationUtil;
import co.cask.cdap.common.conf.Constants;
import co.cask.cdap.common.io.Locations;
import co.cask.cdap.common.lang.ClassLoaders;
import co.cask.cdap.common.lang.CombineClassLoader;
import co.cask.cdap.common.lang.WeakReferenceDelegatorClassLoader;
import co.cask.cdap.common.logging.LoggingContextAccessor;
import co.cask.cdap.common.twill.HadoopClassExcluder;
import co.cask.cdap.common.utils.DirUtils;
import co.cask.cdap.data.stream.StreamInputFormat;
import co.cask.cdap.data.stream.StreamInputFormatProvider;
import co.cask.cdap.data2.metadata.lineage.AccessType;
import co.cask.cdap.data2.registry.UsageRegistry;
import co.cask.cdap.data2.transaction.Transactions;
import co.cask.cdap.data2.transaction.stream.StreamAdmin;
import co.cask.cdap.data2.util.hbase.HBaseTableUtilFactory;
import co.cask.cdap.internal.app.runtime.LocalizationUtils;
import co.cask.cdap.internal.app.runtime.batch.dataset.UnsupportedOutputFormat;
import co.cask.cdap.internal.app.runtime.batch.dataset.input.MapperInput;
import co.cask.cdap.internal.app.runtime.batch.dataset.input.MultipleInputs;
import co.cask.cdap.internal.app.runtime.batch.dataset.output.MultipleOutputs;
import co.cask.cdap.internal.app.runtime.batch.dataset.output.MultipleOutputsMainOutputWrapper;
import co.cask.cdap.internal.app.runtime.batch.distributed.ContainerLauncherGenerator;
import co.cask.cdap.internal.app.runtime.batch.distributed.MapReduceContainerHelper;
import co.cask.cdap.internal.app.runtime.batch.distributed.MapReduceContainerLauncher;
import co.cask.cdap.internal.app.runtime.distributed.LocalizeResource;
import co.cask.cdap.proto.Id;
import co.cask.cdap.proto.ProgramType;
import co.cask.tephra.Transaction;
import co.cask.tephra.TransactionContext;
import co.cask.tephra.TransactionFailureException;
import co.cask.tephra.TransactionSystemClient;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Sets;
import com.google.common.io.ByteStreams;
import com.google.common.io.Files;
import com.google.common.reflect.TypeToken;
import com.google.common.util.concurrent.AbstractExecutionThreadService;
import com.google.inject.Injector;
import com.google.inject.ProvisionException;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.mapreduce.InputFormat;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.MRJobConfig;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.OutputFormat;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.security.Credentials;
import org.apache.hadoop.security.UserGroupInformation;
import org.apache.hadoop.yarn.conf.YarnConfiguration;
import org.apache.twill.api.ClassAcceptor;
import org.apache.twill.filesystem.Location;
import org.apache.twill.filesystem.LocationFactory;
import org.apache.twill.internal.ApplicationBundler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.regex.Pattern;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;

/**
 * Performs the actual execution of mapreduce job.
 *
 * Service start -> Performs job setup, beforeSubmit and submit job
 * Service run -> Poll for job completion
 * Service triggerStop -> kill job
 * Service stop -> Commit/abort transaction, onFinish, cleanup
 */
final class MapReduceRuntimeService extends AbstractExecutionThreadService {

    private static final Logger LOG = LoggerFactory.getLogger(MapReduceRuntimeService.class);

    /**
     * Do not remove: we need this variable for loading MRClientSecurityInfo class required for communicating with
     * AM in secure mode.
     */
    @SuppressWarnings("unused")
    private org.apache.hadoop.mapreduce.v2.app.MRClientSecurityInfo mrClientSecurityInfo;

    // Regex pattern for configuration source if it is set programmatically. This constant is not defined in Hadoop
    // Hadoop 2.3.0 and before has a typo as 'programatically', while it is fixed later as 'programmatically'.
    private static final Pattern PROGRAMATIC_SOURCE_PATTERN = Pattern.compile("program{1,2}atically");

    private final Injector injector;
    private final CConfiguration cConf;
    private final Configuration hConf;
    private final MapReduce mapReduce;
    private final MapReduceSpecification specification;
    private final Location programJarLocation;
    private final BasicMapReduceContext context;
    private final LocationFactory locationFactory;
    private final StreamAdmin streamAdmin;
    private final TransactionSystemClient txClient;
    private final UsageRegistry usageRegistry;

    private Job job;
    private Transaction transaction;
    private Runnable cleanupTask;

    // This needs to keep as a field.
    // We need to hold a strong reference to the ClassLoader until the end of the MapReduce job.
    private ClassLoader classLoader;
    private volatile boolean stopRequested;

    MapReduceRuntimeService(Injector injector, CConfiguration cConf, Configuration hConf, MapReduce mapReduce,
            MapReduceSpecification specification, BasicMapReduceContext context, Location programJarLocation,
            LocationFactory locationFactory, StreamAdmin streamAdmin, TransactionSystemClient txClient,
            UsageRegistry usageRegistry) {
        this.injector = injector;
        this.cConf = cConf;
        this.hConf = hConf;
        this.mapReduce = mapReduce;
        this.specification = specification;
        this.programJarLocation = programJarLocation;
        this.locationFactory = locationFactory;
        this.streamAdmin = streamAdmin;
        this.txClient = txClient;
        this.context = context;
        this.usageRegistry = usageRegistry;
    }

    @Override
    protected String getServiceName() {
        return "MapReduceRunner-" + specification.getName();
    }

    @Override
    protected void startUp() throws Exception {
        // Creates a temporary directory locally for storing all generated files.
        File tempDir = createTempDirectory();
        cleanupTask = createCleanupTask(tempDir);

        try {
            Job job = createJob(new File(tempDir, "mapreduce"));
            Configuration mapredConf = job.getConfiguration();

            classLoader = new MapReduceClassLoader(injector, cConf, mapredConf,
                    context.getProgram().getClassLoader(), context.getPlugins(), context.getPluginInstantiator());
            cleanupTask = createCleanupTask(cleanupTask, classLoader);

            mapredConf.setClassLoader(new WeakReferenceDelegatorClassLoader(classLoader));
            ClassLoaders.setContextClassLoader(mapredConf.getClassLoader());

            context.setJob(job);

            beforeSubmit(job);

            // Localize additional resources that users have requested via BasicMapReduceContext.localize methods
            Map<String, String> localizedUserResources = localizeUserResources(job, tempDir);

            // Override user-defined job name, since we set it and depend on the name.
            // https://issues.cask.co/browse/CDAP-2441
            String jobName = job.getJobName();
            if (!jobName.isEmpty()) {
                LOG.warn("Job name {} is being overridden.", jobName);
            }
            job.setJobName(getJobName(context));

            // Create a temporary location for storing all generated files through the LocationFactory.
            Location tempLocation = createTempLocationDirectory();
            cleanupTask = createCleanupTask(cleanupTask, tempLocation);

            // For local mode, everything is in the configuration classloader already, hence no need to create new jar
            if (!MapReduceTaskContextProvider.isLocal(mapredConf)) {
                // After calling beforeSubmit, we know what plugins are needed for the program, hence construct the proper
                // ClassLoader from here and use it for setting up the job
                Location pluginArchive = createPluginArchive(tempLocation);
                if (pluginArchive != null) {
                    job.addCacheArchive(pluginArchive.toURI());
                    mapredConf.set(Constants.Plugin.ARCHIVE, pluginArchive.getName());
                }
            }

            // set resources for the job
            TaskType.MAP.setResources(mapredConf, context.getMapperResources());
            TaskType.REDUCE.setResources(mapredConf, context.getReducerResources());

            // replace user's Mapper & Reducer's with our wrappers in job config
            MapperWrapper.wrap(job);
            ReducerWrapper.wrap(job);

            // packaging job jar which includes cdap classes with dependencies
            File jobJar = buildJobJar(job, tempDir);
            job.setJar(jobJar.toURI().toString());

            Location programJar = programJarLocation;
            if (!MapReduceTaskContextProvider.isLocal(mapredConf)) {
                // Copy and localize the program jar in distributed mode
                programJar = copyProgramJar(tempLocation);
                job.addCacheFile(programJar.toURI());

                List<String> classpath = new ArrayList<>();

                // Localize logback.xml
                Location logbackLocation = createLogbackJar(tempLocation);
                if (logbackLocation != null) {
                    job.addCacheFile(logbackLocation.toURI());
                    classpath.add(logbackLocation.getName());
                }

                // Generate and localize the launcher jar to control the classloader of MapReduce containers processes
                classpath.add("job.jar/lib/*");
                classpath.add("job.jar/classes");
                Location launcherJar = createLauncherJar(
                        Joiner.on(",").join(MapReduceContainerHelper.getMapReduceClassPath(mapredConf, classpath)),
                        tempLocation);
                job.addCacheFile(launcherJar.toURI());

                // The only thing in the container classpath is the launcher.jar
                // The MapReduceContainerLauncher inside the launcher.jar will creates a MapReduceClassLoader and launch
                // the actual MapReduce AM/Task from that
                // We explicitly localize the mr-framwork, but not use it with the classpath
                URI frameworkURI = MapReduceContainerHelper.getFrameworkURI(mapredConf);
                if (frameworkURI != null) {
                    job.addCacheArchive(frameworkURI);
                }

                mapredConf.unset(MRJobConfig.MAPREDUCE_APPLICATION_FRAMEWORK_PATH);
                mapredConf.set(MRJobConfig.MAPREDUCE_APPLICATION_CLASSPATH, launcherJar.getName());
                mapredConf.set(YarnConfiguration.YARN_APPLICATION_CLASSPATH, launcherJar.getName());
            }

            MapReduceContextConfig contextConfig = new MapReduceContextConfig(mapredConf);
            // We start long-running tx to be used by mapreduce job tasks.
            Transaction tx = txClient.startLong();
            try {
                // We remember tx, so that we can re-use it in mapreduce tasks
                CConfiguration cConfCopy = cConf;
                contextConfig.set(context, cConfCopy, tx, programJar.toURI(), localizedUserResources);

                LOG.info("Submitting MapReduce Job: {}", context);
                // submits job and returns immediately. Shouldn't need to set context ClassLoader.
                job.submit();

                this.job = job;
                this.transaction = tx;
            } catch (Throwable t) {
                Transactions.invalidateQuietly(txClient, tx);
                throw t;
            }
        } catch (Throwable t) {
            LOG.error("Exception when submitting MapReduce Job: {}", context, t);
            cleanupTask.run();
            throw t;
        }
    }

    @Override
    protected void run() throws Exception {
        MapReduceMetricsWriter metricsWriter = new MapReduceMetricsWriter(job, context);

        // until job is complete report stats
        while (!job.isComplete()) {
            metricsWriter.reportStats();

            // we report to metrics backend every second, so 1 sec is enough here. That's mapreduce job anyways (not
            // short) ;)
            TimeUnit.SECONDS.sleep(1);
        }

        LOG.info("MapReduce Job is complete, status: {}, job: {}", job.isSuccessful(), context);
        // NOTE: we want to report the final stats (they may change since last report and before job completed)
        metricsWriter.reportStats();
        // If we don't sleep, the final stats may not get sent before shutdown.
        TimeUnit.SECONDS.sleep(2L);

        // If the job is not successful, throw exception so that this service will terminate with a failure state
        // Shutdown will still get executed, but the service will notify failure after that.
        // However, if it's the job is requested to stop (via triggerShutdown, meaning it's a user action), don't throw
        if (!stopRequested) {
            Preconditions.checkState(job.isSuccessful(), "MapReduce execution failure: %s", job.getStatus());
        }
    }

    @Override
    protected void shutDown() throws Exception {
        boolean success = job.isSuccessful();

        try {
            if (success) {
                LOG.info("Committing MapReduce Job transaction: {}", context);
                // committing long running tx: no need to commit datasets, as they were committed in external processes
                // also no need to rollback changes if commit fails, as these changes where performed by mapreduce tasks
                // NOTE: can't call afterCommit on datasets in this case: the changes were made by external processes.
                if (!txClient.commit(transaction)) {
                    LOG.warn("MapReduce Job transaction failed to commit");
                    throw new TransactionFailureException(
                            "Failed to commit transaction for MapReduce " + context.toString());
                }
            } else {
                // invalids long running tx. All writes done by MR cannot be undone at this point.
                txClient.invalidate(transaction.getWritePointer());
            }
        } finally {
            // whatever happens we want to call this
            try {
                onFinish(success);
            } finally {
                context.close();
                cleanupTask.run();
            }
        }
    }

    @Override
    protected void triggerShutdown() {
        try {
            stopRequested = true;
            if (job != null && !job.isComplete()) {
                job.killJob();
            }
        } catch (IOException e) {
            LOG.error("Failed to kill MapReduce job {}", context, e);
            throw Throwables.propagate(e);
        }
    }

    @Override
    protected Executor executor() {
        // Always execute in new daemon thread.
        return new Executor() {
            @Override
            public void execute(@Nonnull final Runnable runnable) {
                final Thread t = new Thread(new Runnable() {

                    @Override
                    public void run() {
                        // note: this sets logging context on the thread level
                        LoggingContextAccessor.setLoggingContext(context.getLoggingContext());
                        runnable.run();
                    }
                });
                t.setDaemon(true);
                t.setName(getServiceName());
                t.start();
            }
        };
    }

    /**
     * Creates a MapReduce {@link Job} instance.
     *
     * @param hadoopTmpDir directory for the "hadoop.tmp.dir" configuration
     */
    private Job createJob(File hadoopTmpDir) throws IOException {
        Job job = Job.getInstance(new Configuration(hConf));
        Configuration jobConf = job.getConfiguration();

        if (MapReduceTaskContextProvider.isLocal(jobConf)) {
            // Set the MR framework local directories inside the given tmp directory.
            // Setting "hadoop.tmp.dir" here has no effect due to Explore Service need to set "hadoop.tmp.dir"
            // as system property for Hive to work in local mode. The variable substitution of hadoop conf
            // gives system property the highest precedence.
            jobConf.set("mapreduce.cluster.local.dir", new File(hadoopTmpDir, "local").getAbsolutePath());
            jobConf.set("mapreduce.jobtracker.system.dir", new File(hadoopTmpDir, "system").getAbsolutePath());
            jobConf.set("mapreduce.jobtracker.staging.root.dir",
                    new File(hadoopTmpDir, "staging").getAbsolutePath());
            jobConf.set("mapreduce.cluster.temp.dir", new File(hadoopTmpDir, "temp").getAbsolutePath());
        }

        if (UserGroupInformation.isSecurityEnabled()) {
            // If runs in secure cluster, this program runner is running in a yarn container, hence not able
            // to get authenticated with the history.
            jobConf.unset("mapreduce.jobhistory.address");
            jobConf.setBoolean(Job.JOB_AM_ACCESS_DISABLED, false);

            Credentials credentials = UserGroupInformation.getCurrentUser().getCredentials();
            LOG.info("Running in secure mode; adding all user credentials: {}", credentials.getAllTokens());
            job.getCredentials().addAll(credentials);
        }
        return job;
    }

    /**
     * Creates a local temporary directory for this MapReduce run.
     */
    private File createTempDirectory() {
        Id.Program programId = context.getProgram().getId();
        File tempDir = new File(cConf.get(Constants.CFG_LOCAL_DATA_DIR), cConf.get(Constants.AppFabric.TEMP_DIR))
                .getAbsoluteFile();
        File runtimeServiceDir = new File(tempDir, "runner");
        File dir = new File(runtimeServiceDir,
                String.format("%s.%s.%s.%s.%s", programId.getType().name().toLowerCase(),
                        programId.getNamespaceId(), programId.getApplicationId(), programId.getId(),
                        context.getRunId().getId()));
        dir.mkdirs();
        return dir;
    }

    /**
     * Creates a temporary directory through the {@link LocationFactory} provided to this class.
     */
    private Location createTempLocationDirectory() throws IOException {
        Id.Program programId = context.getProgram().getId();

        String tempLocationName = String.format("%s/%s.%s.%s.%s.%s", cConf.get(Constants.AppFabric.TEMP_DIR),
                programId.getType().name().toLowerCase(), programId.getNamespaceId(), programId.getApplicationId(),
                programId.getId(), context.getRunId().getId());
        Location location = locationFactory.create(tempLocationName);
        location.mkdirs();
        return location;
    }

    /**
     * Calls the {@link MapReduce#beforeSubmit(MapReduceContext)} method and
     * also setup the Input/Output within the same transaction.
     */
    private void beforeSubmit(final Job job) throws TransactionFailureException {
        TransactionContext txContext = context.getTransactionContext();
        Transactions.execute(txContext, "beforeSubmit", new Callable<Void>() {
            @Override
            public Void call() throws Exception {
                ClassLoader oldClassLoader = setContextCombinedClassLoader(context);
                try {
                    mapReduce.beforeSubmit(context);

                    // set input/outputs info, and get one of the configured mapper's TypeToken
                    TypeToken<?> mapperTypeToken = setInputsIfNeeded(job);
                    setOutputsIfNeeded(job);

                    setOutputClassesIfNeeded(job, mapperTypeToken);
                    setMapOutputClassesIfNeeded(job, mapperTypeToken);

                    return null;
                } finally {
                    ClassLoaders.setContextClassLoader(oldClassLoader);
                }
            }
        });
    }

    /**
     * Commit a single output after the MR has finished, if it is an OutputFormatCommitter.
     * @param succeeded whether the run was successful
     * @param outputName the name of the output
     * @param outputFormatProvider the output format provider to commit
     * @return whether the action was successful (it did not throw an exception)
     */
    private boolean commitOutput(boolean succeeded, String outputName, OutputFormatProvider outputFormatProvider) {
        if (outputFormatProvider instanceof DatasetOutputCommitter) {
            try {
                if (succeeded) {
                    ((DatasetOutputCommitter) outputFormatProvider).onSuccess();
                } else {
                    ((DatasetOutputCommitter) outputFormatProvider).onFailure();
                }
            } catch (Throwable t) {
                LOG.error(String.format("Error from %s method of output dataset %s.",
                        succeeded ? "onSuccess" : "onFailure", outputName), t);
                return false;
            }
        }
        return true;
    }

    /**
     * Calls the {@link MapReduce#onFinish(boolean, co.cask.cdap.api.mapreduce.MapReduceContext)} method.
     */
    private void onFinish(final boolean succeeded) throws TransactionFailureException {
        TransactionContext txContext = context.getTransactionContext();
        Transactions.execute(txContext, "onFinish", new Callable<Void>() {
            @Override
            public Void call() throws Exception {
                ClassLoader oldClassLoader = setContextCombinedClassLoader(context);
                try {
                    // TODO (CDAP-1952): this should be done in the output committer, to make the M/R fail if addPartition fails
                    // TODO (CDAP-1952): also, should failure of an output committer change the status of the program run?
                    boolean success = succeeded;
                    for (Map.Entry<String, OutputFormatProvider> dsEntry : context.getOutputFormatProviders()
                            .entrySet()) {
                        if (!commitOutput(succeeded, dsEntry.getKey(), dsEntry.getValue())) {
                            success = false;
                        }
                    }
                    mapReduce.onFinish(success, context);
                    return null;
                } finally {
                    ClassLoaders.setContextClassLoader(oldClassLoader);
                }
            }
        });
    }

    private void assertConsistentTypes(Class<? extends Mapper> firstMapperClass,
            Map.Entry<Class, Class> firstMapperClassOutputTypes, Class<? extends Mapper> secondMapperClass) {
        Map.Entry<Class, Class> mapperOutputKeyValueTypes = getMapperOutputKeyValueTypes(secondMapperClass);
        if (!firstMapperClassOutputTypes.getKey().equals(mapperOutputKeyValueTypes.getKey())
                || !firstMapperClassOutputTypes.getValue().equals(mapperOutputKeyValueTypes.getValue())) {
            throw new IllegalArgumentException(String.format(
                    "Type mismatch in output type of mappers: %s and %s. " + "Map output key types: %s and %s. "
                            + "Map output value types: %s and %s.",
                    firstMapperClass, secondMapperClass, firstMapperClassOutputTypes.getKey(),
                    mapperOutputKeyValueTypes.getKey(), firstMapperClassOutputTypes.getValue(),
                    mapperOutputKeyValueTypes.getValue()));
        }
    }

    private Map.Entry<Class, Class> getMapperOutputKeyValueTypes(Class<? extends Mapper> mapperClass) {
        TypeToken<Mapper> firstType = resolveClass(mapperClass, Mapper.class);
        Type[] firstTypeArgs = ((ParameterizedType) firstType.getType()).getActualTypeArguments();
        return new AbstractMap.SimpleEntry<Class, Class>(TypeToken.of(firstTypeArgs[2]).getRawType(),
                TypeToken.of(firstTypeArgs[3]).getRawType());
    }

    /**
     * Sets the configurations used for inputs.
     * Multiple mappers could be defined, so we first check that their output types are consistent.
     *
     * @return the TypeToken for one of the mappers (doesn't matter which one, since we check that all of their output
     * key/value types are consistent. Returns null if the mapper class was not configured directly on the job and the
     * job's mapper class is to be used.
     * @throws IllegalArgumentException if any of the configured mapper output types are inconsistent.
     */
    @Nullable
    private TypeToken<Mapper> setInputsIfNeeded(Job job) throws IOException, ClassNotFoundException {
        Class<? extends Mapper> jobMapperClass = job.getMapperClass();

        Class<? extends Mapper> firstMapperClass = null;
        Map.Entry<Class, Class> firstMapperOutputTypes = null;

        for (Map.Entry<String, MapperInput> mapperInputEntry : context.getMapperInputs().entrySet()) {
            MapperInput mapperInput = mapperInputEntry.getValue();
            InputFormatProvider provider = mapperInput.getInputFormatProvider();
            Map<String, String> inputFormatConfiguration = new HashMap<>(provider.getInputFormatConfiguration());

            // default to what is configured on the job, if user didn't specify a mapper for an input
            Class<? extends Mapper> mapperClass = mapperInput.getMapper() == null ? jobMapperClass
                    : mapperInput.getMapper();

            // check output key/value type consistency, except for the first input
            if (firstMapperClass == null) {
                firstMapperClass = mapperClass;
                firstMapperOutputTypes = getMapperOutputKeyValueTypes(mapperClass);
            } else {
                assertConsistentTypes(firstMapperClass, firstMapperOutputTypes, mapperClass);
            }

            // A bit hacky for stream.
            if (provider instanceof StreamInputFormatProvider) {
                // pass in mapperInput.getMapper() instead of mapperClass, because mapperClass defaults to the Identity Mapper
                setDecoderForStream((StreamInputFormatProvider) provider, job, inputFormatConfiguration,
                        mapperInput.getMapper());
            }

            MultipleInputs.addInput(job, mapperInputEntry.getKey(), provider.getInputFormatClassName(),
                    inputFormatConfiguration, mapperClass);
        }

        // if firstMapperClass is null, then, user is not going through our APIs to add input; leave the job's input format
        // to user and simply return the mapper output types of the mapper configured on the job.
        // if firstMapperClass == jobMapperClass, return null if the user didn't configure the mapper class explicitly
        if (firstMapperClass == null || firstMapperClass == jobMapperClass) {
            return resolveClass(job.getConfiguration(), MRJobConfig.MAP_CLASS_ATTR, Mapper.class);
        }
        return resolveClass(firstMapperClass, Mapper.class);
    }

    private void setDecoderForStream(StreamInputFormatProvider streamProvider, Job job,
            Map<String, String> inputFormatConfiguration, Class<? extends Mapper> mapperClass) {
        // For stream, we need to do two extra steps.
        // 1. stream usage registration since it only happens on client side.
        // 2. Infer the stream event decoder from Mapper/Reducer
        TypeToken<?> mapperTypeToken = mapperClass == null ? null : resolveClass(mapperClass, Mapper.class);
        Type inputValueType = getInputValueType(job.getConfiguration(), StreamEvent.class, mapperTypeToken);
        streamProvider.setDecoderType(inputFormatConfiguration, inputValueType);

        Id.Stream streamId = streamProvider.getStreamId();
        try {
            usageRegistry.register(context.getProgram().getId(), streamId);
            streamAdmin.addAccess(new Id.Run(context.getProgram().getId(), context.getRunId().getId()), streamId,
                    AccessType.READ);
        } catch (Exception e) {
            LOG.warn("Failed to register usage {} -> {}", context.getProgram().getId(), streamId, e);
        }
    }

    /**
     * Sets the configurations used for outputs.
     */
    private void setOutputsIfNeeded(Job job) {
        Map<String, OutputFormatProvider> outputFormatProviders = context.getOutputFormatProviders();
        LOG.debug("Using as output for MapReduce Job: {}", outputFormatProviders.keySet());

        if (outputFormatProviders.isEmpty()) {
            // user is not going through our APIs to add output; leave the job's output format to user
            return;
        } else if (outputFormatProviders.size() == 1) {
            // If only one output is configured through the context, then set it as the root OutputFormat
            Map.Entry<String, OutputFormatProvider> next = outputFormatProviders.entrySet().iterator().next();
            OutputFormatProvider outputFormatProvider = next.getValue();
            ConfigurationUtil.setAll(outputFormatProvider.getOutputFormatConfiguration(), job.getConfiguration());
            job.getConfiguration().set(Job.OUTPUT_FORMAT_CLASS_ATTR,
                    outputFormatProvider.getOutputFormatClassName());
            return;
        }
        // multiple output formats configured via the context. We should use a RecordWriter that doesn't support writing
        // as the root output format in this case to disallow writing directly on the context
        MultipleOutputsMainOutputWrapper.setRootOutputFormat(job, UnsupportedOutputFormat.class.getName(),
                new HashMap<String, String>());
        job.setOutputFormatClass(MultipleOutputsMainOutputWrapper.class);

        for (Map.Entry<String, OutputFormatProvider> entry : outputFormatProviders.entrySet()) {
            String outputName = entry.getKey();
            OutputFormatProvider outputFormatProvider = entry.getValue();

            String outputFormatClassName = outputFormatProvider.getOutputFormatClassName();
            if (outputFormatClassName == null) {
                throw new IllegalArgumentException(
                        "Output '" + outputName + "' provided null as the output format");
            }

            Map<String, String> outputConfig = outputFormatProvider.getOutputFormatConfiguration();
            MultipleOutputs.addNamedOutput(job, outputName, outputFormatClassName, job.getOutputKeyClass(),
                    job.getOutputValueClass(), outputConfig);

        }
    }

    /**
     * Returns the input value type of the MR job based on the job Mapper/Reducer type.
     * It does so by inspecting the Mapper/Reducer type parameters to figure out what the input type is.
     * If the job has Mapper, then it's the Mapper IN_VALUE type, otherwise it would be the Reducer IN_VALUE type.
     * If the cannot determine the input value type, then return the given default type.
     *
     * @param hConf the Configuration to use to resolve the class TypeToken
     * @param defaultType the defaultType to return
     * @param mapperTypeToken the mapper type token for the configured input (not resolved by the job's mapper class)
     */
    @VisibleForTesting
    static Type getInputValueType(Configuration hConf, Type defaultType, @Nullable TypeToken<?> mapperTypeToken) {
        TypeToken<?> type;
        if (mapperTypeToken == null) {
            // if the input's mapper is null, first try resolving a from the job
            mapperTypeToken = resolveClass(hConf, MRJobConfig.MAP_CLASS_ATTR, Mapper.class);
        }

        if (mapperTypeToken == null) {
            // If there is no Mapper, it's a Reducer only job, hence get the value type from Reducer class
            type = resolveClass(hConf, MRJobConfig.REDUCE_CLASS_ATTR, Reducer.class);
        } else {
            type = mapperTypeToken;
        }
        Preconditions.checkArgument(type != null,
                "Neither a Mapper nor a Reducer is configured for the MapReduce job.");

        if (!(type.getType() instanceof ParameterizedType)) {
            return defaultType;
        }

        // The super type Mapper/Reducer must be a parametrized type with <IN_KEY, IN_VALUE, OUT_KEY, OUT_VALUE>
        Type inputValueType = ((ParameterizedType) type.getType()).getActualTypeArguments()[1];

        // If the concrete Mapper/Reducer class is not parameterized (meaning not extends with parameters),
        // then assume use the default type.
        // We need to check if the TypeVariable is the same as the one in the parent type.
        // This avoid the case where a subclass that has "class InvalidMapper<I, O> extends Mapper<I, O>"
        if (inputValueType instanceof TypeVariable
                && inputValueType.equals(type.getRawType().getTypeParameters()[1])) {
            inputValueType = defaultType;
        }
        return inputValueType;
    }

    private String getJobName(BasicMapReduceContext context) {
        Id.Program programId = context.getProgram().getId();
        // MRJobClient expects the following format (for RunId to be the first component)
        return String.format("%s.%s.%s.%s.%s", context.getRunId().getId(),
                ProgramType.MAPREDUCE.name().toLowerCase(), programId.getNamespaceId(),
                programId.getApplicationId(), programId.getId());
    }

    /**
     * Creates a jar that contains everything that are needed for running the MapReduce program by Hadoop.
     *
     * @return a new {@link File} containing the job jar
     */
    private File buildJobJar(Job job, File tempDir) throws IOException, URISyntaxException {
        File jobJar = new File(tempDir, "job.jar");
        LOG.debug("Creating Job jar: {}", jobJar);

        // For local mode, nothing is needed in the job jar since we use the classloader in the configuration object.
        if (MapReduceTaskContextProvider.isLocal(job.getConfiguration())) {
            JarOutputStream output = new JarOutputStream(new FileOutputStream(jobJar));
            output.close();
            return jobJar;
        }

        // Excludes libraries that are for sure not needed.
        // Hadoop - Available from the cluster
        // Spark - MR never uses Spark
        final HadoopClassExcluder hadoopClassExcluder = new HadoopClassExcluder();
        ApplicationBundler appBundler = new ApplicationBundler(new ClassAcceptor() {
            @Override
            public boolean accept(String className, URL classUrl, URL classPathUrl) {
                if (className.startsWith("org.apache.spark")
                        || classPathUrl.toString().contains("spark-assembly")) {
                    return false;
                }
                return hadoopClassExcluder.accept(className, classUrl, classPathUrl);
            }
        });
        Set<Class<?>> classes = Sets.newHashSet();
        classes.add(MapReduce.class);
        classes.add(MapperWrapper.class);
        classes.add(ReducerWrapper.class);

        // We only need to trace the Input/OutputFormat class due to MAPREDUCE-5957 so that those classes are included
        // in the job.jar and be available in the MR system classpath before our job classloader (ApplicationClassLoader)
        // take over the classloading.
        if (cConf.getBoolean(Constants.AppFabric.MAPREDUCE_INCLUDE_CUSTOM_CLASSES)) {
            try {
                Class<? extends InputFormat<?, ?>> inputFormatClass = job.getInputFormatClass();
                LOG.info("InputFormat class: {} {}", inputFormatClass, inputFormatClass.getClassLoader());
                classes.add(inputFormatClass);

                // If it is StreamInputFormat, also add the StreamEventCodec class as well.
                if (StreamInputFormat.class.isAssignableFrom(inputFormatClass)) {
                    Class<? extends StreamEventDecoder> decoderType = StreamInputFormat
                            .getDecoderClass(job.getConfiguration());
                    if (decoderType != null) {
                        classes.add(decoderType);
                    }
                }
            } catch (Throwable t) {
                LOG.info("InputFormat class not found: {}", t.getMessage(), t);
                // Ignore
            }
            try {
                Class<? extends OutputFormat<?, ?>> outputFormatClass = job.getOutputFormatClass();
                LOG.info("OutputFormat class: {} {}", outputFormatClass, outputFormatClass.getClassLoader());
                classes.add(outputFormatClass);
            } catch (Throwable t) {
                LOG.info("OutputFormat class not found: {}", t.getMessage(), t);
                // Ignore
            }
        }
        // End of MAPREDUCE-5957.

        try {
            Class<?> hbaseTableUtilClass = HBaseTableUtilFactory.getHBaseTableUtilClass();
            classes.add(hbaseTableUtilClass);
        } catch (ProvisionException e) {
            LOG.warn("Not including HBaseTableUtil classes in submitted Job Jar since they are not available");
        }

        ClassLoader oldCLassLoader = ClassLoaders.setContextClassLoader(job.getConfiguration().getClassLoader());
        appBundler.createBundle(Locations.toLocation(jobJar), classes);
        ClassLoaders.setContextClassLoader(oldCLassLoader);

        LOG.info("Built MapReduce Job Jar at {}", jobJar.toURI());
        return jobJar;
    }

    /**
     * Returns a resolved {@link TypeToken} of the given super type by reading a class from the job configuration that
     * extends from super type.
     *
     * @param conf the job configuration
     * @param typeAttr The job configuration attribute for getting the user class
     * @param superType Super type of the class to get from the configuration
     * @param <V> Type of the super type
     * @return A resolved {@link TypeToken} or {@code null} if no such class in the job configuration
     */
    @SuppressWarnings("unchecked")
    @VisibleForTesting
    @Nullable
    static <V> TypeToken<V> resolveClass(Configuration conf, String typeAttr, Class<V> superType) {
        Class<? extends V> userClass = conf.getClass(typeAttr, null, superType);
        if (userClass == null) {
            return null;
        }
        return resolveClass(userClass, superType);
    }

    /**
     * Returns a resolved {@link TypeToken} of the given super type of the class.
     *
     * @param userClass the user class of which we want the TypeToken
     * @param superType Super type of the class
     * @param <V> Type of the super type
     * @return A resolved {@link TypeToken}
     */
    @SuppressWarnings("unchecked")
    private static <V> TypeToken<V> resolveClass(Class<? extends V> userClass, Class<V> superType) {
        return (TypeToken<V>) TypeToken.of(userClass).getSupertype(superType);
    }

    /**
     * Sets the output key and value classes in the job configuration by inspecting the {@link Mapper} and {@link Reducer}
     * if it is not set by the user.
     *
     * @param job the MapReduce job
     * @param mapperTypeToken TypeToken of a configured mapper (may not be configured on the job). Has already been
     *                        resolved from the job's mapper class.
     */
    private void setOutputClassesIfNeeded(Job job, @Nullable TypeToken<?> mapperTypeToken) {
        Configuration conf = job.getConfiguration();

        // Try to get the type from reducer
        TypeToken<?> type = resolveClass(conf, MRJobConfig.REDUCE_CLASS_ATTR, Reducer.class);

        if (type == null) {
            // Map only job
            type = mapperTypeToken;
        }

        // If not able to detect type, nothing to set
        if (type == null || !(type.getType() instanceof ParameterizedType)) {
            return;
        }

        Type[] typeArgs = ((ParameterizedType) type.getType()).getActualTypeArguments();

        // Set it only if the user didn't set it in beforeSubmit
        // The key and value type are in the 3rd and 4th type parameters
        if (!isProgrammaticConfig(conf, MRJobConfig.OUTPUT_KEY_CLASS)) {
            Class<?> cls = TypeToken.of(typeArgs[2]).getRawType();
            LOG.debug("Set output key class to {}", cls);
            job.setOutputKeyClass(cls);
        }
        if (!isProgrammaticConfig(conf, MRJobConfig.OUTPUT_VALUE_CLASS)) {
            Class<?> cls = TypeToken.of(typeArgs[3]).getRawType();
            LOG.debug("Set output value class to {}", cls);
            job.setOutputValueClass(cls);
        }
    }

    /**
     * Sets the map output key and value classes in the job configuration by inspecting the {@link Mapper}
     * if it is not set by the user.
     *
     * @param job the MapReduce job
     * @param mapperTypeToken TypeToken of a configured mapper (may not be configured on the job). Has already been
     *                        resolved from the job's mapper class.
     */
    private void setMapOutputClassesIfNeeded(Job job, @Nullable TypeToken<?> mapperTypeToken) {
        Configuration conf = job.getConfiguration();

        TypeToken<?> type = mapperTypeToken;
        int keyIdx = 2;
        int valueIdx = 3;

        if (type == null) {
            // Reducer only job. Use the Reducer input types as the key/value classes.
            type = resolveClass(conf, MRJobConfig.REDUCE_CLASS_ATTR, Reducer.class);
            keyIdx = 0;
            valueIdx = 1;
        }

        // If not able to detect type, nothing to set.
        if (type == null || !(type.getType() instanceof ParameterizedType)) {
            return;
        }

        Type[] typeArgs = ((ParameterizedType) type.getType()).getActualTypeArguments();

        // Set it only if the user didn't set it in beforeSubmit
        // The key and value type are in the 3rd and 4th type parameters
        if (!isProgrammaticConfig(conf, MRJobConfig.MAP_OUTPUT_KEY_CLASS)) {
            Class<?> cls = TypeToken.of(typeArgs[keyIdx]).getRawType();
            LOG.debug("Set map output key class to {}", cls);
            job.setMapOutputKeyClass(cls);
        }
        if (!isProgrammaticConfig(conf, MRJobConfig.MAP_OUTPUT_VALUE_CLASS)) {
            Class<?> cls = TypeToken.of(typeArgs[valueIdx]).getRawType();
            LOG.debug("Set map output value class to {}", cls);
            job.setMapOutputValueClass(cls);
        }
    }

    private boolean isProgrammaticConfig(Configuration conf, String name) {
        String[] sources = conf.getPropertySources(name);
        return sources != null && sources.length > 0
                && PROGRAMATIC_SOURCE_PATTERN.matcher(sources[sources.length - 1]).matches();
    }

    /**
     * Copies a plugin archive jar to the target location.
     *
     * @param targetDir directory where the archive jar should be created
     * @return {@link Location} to the plugin archive or {@code null} if no plugin archive is available from the context.
     */
    @Nullable
    private Location createPluginArchive(Location targetDir) throws IOException {
        File pluginArchive = context.getPluginArchive();
        if (pluginArchive == null) {
            return null;
        }
        Location pluginLocation = targetDir.append(pluginArchive.getName()).getTempFile(".jar");
        Files.copy(pluginArchive, Locations.newOutputSupplier(pluginLocation));
        return pluginLocation;
    }

    /**
     * Creates a jar in the given directory that contains a logback.xml loaded from the current ClassLoader.
     *
     * @param targetDir directory where the logback.xml should be copied to
     * @return the {@link Location} where the logback.xml jar copied to or {@code null} if "logback.xml" is not found
     *         in the current ClassLoader.
     */
    @Nullable
    private Location createLogbackJar(Location targetDir) throws IOException {
        try (InputStream input = Thread.currentThread().getContextClassLoader()
                .getResourceAsStream("logback.xml")) {
            if (input != null) {
                Location logbackJar = targetDir.append("logback").getTempFile(".jar");
                try (JarOutputStream output = new JarOutputStream(logbackJar.getOutputStream())) {
                    output.putNextEntry(new JarEntry("logback.xml"));
                    ByteStreams.copy(input, output);
                }
                return logbackJar;
            } else {
                LOG.warn("Could not find logback.xml for MapReduce!");
            }
        }
        return null;
    }

    /**
     * Creates a temp copy of the program jar.
     *
     * @return a new {@link Location} which contains the same content as the program jar
     */
    private Location copyProgramJar(Location targetDir) throws IOException {
        Location programJarCopy = targetDir.append("program.jar");

        ByteStreams.copy(Locations.newInputSupplier(programJarLocation),
                Locations.newOutputSupplier(programJarCopy));
        LOG.info("Copied Program Jar to {}, source: {}", programJarCopy, programJarLocation);
        return programJarCopy;
    }

    /**
     * Creates a launcher jar.
     *
     * @see MapReduceContainerLauncher
     * @see ContainerLauncherGenerator
     */
    private Location createLauncherJar(String applicationClassPath, Location targetDir) throws IOException {
        Location launcherJar = targetDir.append("launcher.jar");
        ContainerLauncherGenerator.generateLauncherJar(applicationClassPath, MapReduceClassLoader.class.getName(),
                Locations.newOutputSupplier(launcherJar));
        return launcherJar;
    }

    private Runnable createCleanupTask(final Object... resources) {
        return new Runnable() {

            @Override
            public void run() {
                for (Object resource : resources) {
                    if (resource == null) {
                        continue;
                    }

                    try {
                        if (resource instanceof File) {
                            if (((File) resource).isDirectory()) {
                                DirUtils.deleteDirectoryContents((File) resource);
                            } else {
                                ((File) resource).delete();
                            }
                        } else if (resource instanceof Location) {
                            Locations.deleteQuietly((Location) resource, true);
                        } else if (resource instanceof AutoCloseable) {
                            ((AutoCloseable) resource).close();
                        } else if (resource instanceof Runnable) {
                            ((Runnable) resource).run();
                        }
                    } catch (Throwable t) {
                        LOG.warn("Exception when cleaning up resource {}", resource, t);
                    }
                }
            }
        };
    }

    private enum TaskType {
        MAP(Job.MAP_MEMORY_MB, Job.MAP_JAVA_OPTS), REDUCE(Job.REDUCE_MEMORY_MB, Job.REDUCE_JAVA_OPTS);

        private final String memoryConfKey;
        private final String javaOptsKey;
        private final String vcoreConfKey;

        TaskType(String memoryConfKey, String javaOptsKey) {
            this.memoryConfKey = memoryConfKey;
            this.javaOptsKey = javaOptsKey;

            String vcoreConfKey = null;
            try {
                String fieldName = name() + "_CPU_VCORES";
                Field field = Job.class.getField(fieldName);
                vcoreConfKey = field.get(null).toString();
            } catch (Exception e) {
                // OK to ignore
                // Some older version of hadoop-mr-client doesn't has the VCORES field as vcores was not supported in YARN.
            }
            this.vcoreConfKey = vcoreConfKey;
        }

        /**
         * Sets up resources usage for the task represented by this task type.
         *
         * @param conf configuration to modify
         * @param resources resources information or {@code null} if nothing to set
         */
        public void setResources(Configuration conf, @Nullable Resources resources) {
            if (resources == null) {
                return;
            }

            conf.setInt(memoryConfKey, resources.getMemoryMB());
            // Also set the Xmx to be smaller than the container memory.
            conf.set(javaOptsKey, "-Xmx" + (int) (resources.getMemoryMB() * 0.8) + "m");

            if (vcoreConfKey != null) {
                conf.setInt(vcoreConfKey, resources.getVirtualCores());
            }
        }
    }

    private ClassLoader setContextCombinedClassLoader(BasicMapReduceContext context) {
        return ClassLoaders.setContextClassLoader(new CombineClassLoader(null,
                ImmutableList.of(context.getProgram().getClassLoader(), getClass().getClassLoader())));
    }

    /**
     * Localizes resources requested by users in the MapReduce Program's beforeSubmit phase.
     * In Local mode, also copies resources to a temporary directory.
     *
     * @param job the {@link Job} for this MapReduce program
     * @param targetDir in local mode, a temporary directory to copy the resources to
     * @return a {@link Map} of resource name to the resource path. The resource path will be absolute in local mode,
     * while it will just contain the file name in distributed mode.
     */
    private Map<String, String> localizeUserResources(Job job, File targetDir) throws IOException {
        Map<String, String> localizedResources = new HashMap<>();
        Map<String, LocalizeResource> resourcesToLocalize = context.getResourcesToLocalize();
        for (Map.Entry<String, LocalizeResource> entry : resourcesToLocalize.entrySet()) {
            String localizedFilePath;
            String name = entry.getKey();
            Configuration mapredConf = job.getConfiguration();
            if (MapReduceTaskContextProvider.isLocal(mapredConf)) {
                // in local mode, also add localize resources in a temporary directory
                localizedFilePath = LocalizationUtils.localizeResource(entry.getKey(), entry.getValue(), targetDir)
                        .getAbsolutePath();
            } else {
                URI uri = entry.getValue().getURI();
                // in distributed mode, use the MapReduce Job object to localize resources
                URI actualURI;
                try {
                    actualURI = new URI(uri.getScheme(), uri.getAuthority(), uri.getPath(), uri.getQuery(), name);
                } catch (URISyntaxException e) {
                    // Most of the URI is constructed from the passed URI. So ideally, this should not happen.
                    // If it does though, there is nothing that clients can do to recover, so not propagating a checked exception.
                    throw Throwables.propagate(e);
                }
                if (entry.getValue().isArchive()) {
                    job.addCacheArchive(actualURI);
                } else {
                    job.addCacheFile(actualURI);
                }
                localizedFilePath = name;
            }
            localizedResources.put(name, localizedFilePath);
        }
        return localizedResources;
    }
}