co.cask.cdap.explore.service.ExploreServiceUtils.java Source code

Java tutorial

Introduction

Here is the source code for co.cask.cdap.explore.service.ExploreServiceUtils.java

Source

/*
 * 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.explore.service;

import co.cask.cdap.common.conf.Constants;
import co.cask.cdap.data2.datafabric.dataset.service.DatasetService;
import co.cask.cdap.data2.util.hbase.HBaseTableUtilFactory;
import co.cask.cdap.explore.guice.ExploreRuntimeModule;
import co.cask.cdap.explore.service.hive.Hive12CDH5ExploreService;
import co.cask.cdap.explore.service.hive.Hive12ExploreService;
import co.cask.cdap.explore.service.hive.Hive13ExploreService;
import co.cask.cdap.explore.service.hive.Hive14ExploreService;
import co.cask.cdap.hive.ExploreUtils;
import co.cask.cdap.internal.app.runtime.spark.SparkUtils;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.io.ByteStreams;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hive.conf.HiveConf;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.MRJobConfig;
import org.apache.hadoop.util.VersionInfo;
import org.apache.hadoop.yarn.conf.YarnConfiguration;
import org.apache.twill.api.ClassAcceptor;
import org.apache.twill.internal.utils.Dependencies;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.commons.GeneratorAdapter;
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.net.MalformedURLException;
import java.net.URL;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.regex.Pattern;
import javax.annotation.Nullable;

/**
 * Utility class for the explore service.
 */
public class ExploreServiceUtils {
    private static final Logger LOG = LoggerFactory.getLogger(ExploreServiceUtils.class);

    private static final String HIVE_AUTHFACTORY_CLASS_NAME = "org.apache.hive.service.auth.HiveAuthFactory";

    /**
     * Hive support enum.
     */
    public enum HiveSupport {
        // The order of the enum values below is very important
        // CDH 5.0 to 5.1 uses Hive 0.12
        HIVE_CDH5_0(Pattern.compile("^.*cdh5.0\\..*$"), Hive12CDH5ExploreService.class), HIVE_CDH5_1(
                Pattern.compile("^.*cdh5.1\\..*$"), Hive12CDH5ExploreService.class),
        // CDH 5.2.x and 5.3.x use Hive 0.13
        HIVE_CDH5_2(Pattern.compile("^.*cdh5.2\\..*$"), Hive13ExploreService.class), HIVE_CDH5_3(
                Pattern.compile("^.*cdh5.3\\..*$"), Hive13ExploreService.class),
        // CDH > 5.3 uses Hive >= 1.1 (which Hive14ExploreService supports)
        HIVE_CDH5(Pattern.compile("^.*cdh5\\..*$"), Hive14ExploreService.class),

        HIVE_12(null, Hive12ExploreService.class), HIVE_13(null, Hive13ExploreService.class), HIVE_14(null,
                Hive14ExploreService.class), HIVE_1_0(null, Hive14ExploreService.class), HIVE_1_1(null,
                        Hive14ExploreService.class), HIVE_1_2(null, Hive14ExploreService.class);

        private final Pattern hadoopVersionPattern;
        private final Class<? extends ExploreService> hiveExploreServiceClass;

        HiveSupport(Pattern hadoopVersionPattern, Class<? extends ExploreService> hiveExploreServiceClass) {
            this.hadoopVersionPattern = hadoopVersionPattern;
            this.hiveExploreServiceClass = hiveExploreServiceClass;
        }

        public Pattern getHadoopVersionPattern() {
            return hadoopVersionPattern;
        }

        public Class<? extends ExploreService> getHiveExploreServiceClass() {
            return hiveExploreServiceClass;
        }
    }

    // Caching the dependencies so that we don't trace them twice
    private static Set<File> exploreDependencies = null;

    private static final Pattern HIVE_SITE_FILE_PATTERN = Pattern.compile("^.*/hive-site\\.xml$");
    private static final Pattern YARN_SITE_FILE_PATTERN = Pattern.compile("^.*/yarn-site\\.xml$");
    private static final Pattern MAPRED_SITE_FILE_PATTERN = Pattern.compile("^.*/mapred-site\\.xml$");

    public static Class<? extends ExploreService> getHiveService() {
        HiveSupport hiveVersion = checkHiveSupport(null);
        return hiveVersion.getHiveExploreServiceClass();
    }

    public static HiveSupport checkHiveSupport() {
        return checkHiveSupport(ExploreUtils.getExploreClassloader());
    }

    public static String getHiveVersion() {
        return getHiveVersion(ExploreUtils.getExploreClassloader());
    }

    public static String getHiveVersion(@Nullable ClassLoader hiveClassLoader) {
        ClassLoader usingCL = hiveClassLoader;
        if (usingCL == null) {
            usingCL = ExploreServiceUtils.class.getClassLoader();
        }

        try {
            Class<?> hiveVersionInfoClass = usingCL.loadClass("org.apache.hive.common.util.HiveVersionInfo");
            return (String) hiveVersionInfoClass.getDeclaredMethod("getVersion").invoke(null);
        } catch (Exception e) {
            throw Throwables.propagate(e);
        }
    }

    /**
     * Check that Hive is in the class path - with a right version.
     */
    public static HiveSupport checkHiveSupport(@Nullable ClassLoader hiveClassLoader) {
        // First try to figure which hive support is relevant based on Hadoop distribution name
        String hadoopVersion = VersionInfo.getVersion();
        for (HiveSupport hiveSupport : HiveSupport.values()) {
            if (hiveSupport.getHadoopVersionPattern() != null
                    && hiveSupport.getHadoopVersionPattern().matcher(hadoopVersion).matches()) {
                return hiveSupport;
            }
        }

        String hiveVersion = getHiveVersion(hiveClassLoader);
        LOG.info("Client Hive version: {}", hiveVersion);
        if (hiveVersion.startsWith("0.12.")) {
            return HiveSupport.HIVE_12;
        } else if (hiveVersion.startsWith("0.13.")) {
            return HiveSupport.HIVE_13;
        } else if (hiveVersion.startsWith("0.14.") || hiveVersion.startsWith("1.0.")) {
            return HiveSupport.HIVE_14;
        } else if (hiveVersion.startsWith("1.1.")) {
            return HiveSupport.HIVE_1_1;
        } else if (hiveVersion.startsWith(("1.2"))) {
            return HiveSupport.HIVE_1_2;
        }

        throw new RuntimeException("Hive distribution not supported. Set the configuration '"
                + Constants.Explore.EXPLORE_ENABLED + "' to false to start up without Explore.");
    }

    /**
     * Return the list of absolute paths of the bootstrap classes.
     */
    public static Set<String> getBoostrapClasses() {
        ImmutableSet.Builder<String> builder = ImmutableSet.builder();
        for (String classpath : Splitter.on(File.pathSeparatorChar)
                .split(System.getProperty("sun.boot.class.path"))) {
            File file = new File(classpath);
            builder.add(file.getAbsolutePath());
            try {
                builder.add(file.getCanonicalPath());
            } catch (IOException e) {
                LOG.warn("Could not add canonical path to aux class path for file {}", file.toString(), e);
            }
        }
        return builder.build();
    }

    /**
     * Trace the jar dependencies needed by the Explore container. Uses a separate class loader to load Hive classes,
     * built using the explore classpath passed as a system property to master.
     *
     * @return an ordered set of jar files.
     */
    public static Set<File> traceExploreDependencies(File tmpDir) throws IOException {
        if (exploreDependencies != null) {
            return exploreDependencies;
        }

        ClassLoader classLoader = ExploreUtils.getExploreClassloader();
        Set<File> additionalJars = new HashSet<>();
        if (isSparkAvailable()) {
            File sparkAssemblyJar = SparkUtils.locateSparkAssemblyJar();
            LOG.debug("Adding spark jar to explore dependency {}", sparkAssemblyJar);
            additionalJars.add(sparkAssemblyJar);
        }
        if (isTezAvailable()) {
            additionalJars.addAll(getTezJars());
        }
        return traceExploreDependencies(classLoader, tmpDir, additionalJars);
    }

    /**
     * Trace the jar dependencies needed by the Explore container.
     *
     * @param classLoader class loader to use to trace the dependencies.
     *                    If it is null, use the class loader of this class.
     * @param tmpDir temporary directory for storing rewritten jar files.
     * @param additionalJars additional jars that will be added to the end of the returned set.
     * @return an ordered set of jar files.
     */
    private static Set<File> traceExploreDependencies(ClassLoader classLoader, File tmpDir,
            Set<File> additionalJars) throws IOException {
        if (exploreDependencies != null) {
            return exploreDependencies;
        }

        ClassLoader usingCL = classLoader;
        if (classLoader == null) {
            usingCL = ExploreRuntimeModule.class.getClassLoader();
        }

        final Set<String> bootstrapClassPaths = getBoostrapClasses();

        ClassAcceptor classAcceptor = new ClassAcceptor() {
            /* Excluding any class contained in the bootstrapClassPaths and Kryo classes.
              * We need to remove Kryo dependency in the Explore container. Spark introduced version 2.21 version of Kryo,
              * which would be normally shipped to the Explore container. Yet, Hive requires Kryo 2.22,
              * and gets it from the Hive jars - hive-exec.jar to be precise.
              * */
            @Override
            public boolean accept(String className, URL classUrl, URL classPathUrl) {
                return !(bootstrapClassPaths.contains(classPathUrl.getFile())
                        || className.startsWith("com.esotericsoftware.kryo"));
            }
        };

        Set<File> hBaseTableDeps = traceDependencies(usingCL, classAcceptor, tmpDir,
                HBaseTableUtilFactory.getHBaseTableUtilClass().getName());

        // Note the order of dependency jars is important so that HBase jars come first in the classpath order
        // LinkedHashSet maintains insertion order while removing duplicate entries.
        Set<File> orderedDependencies = new LinkedHashSet<>();
        orderedDependencies.addAll(hBaseTableDeps);
        orderedDependencies.addAll(traceDependencies(usingCL, classAcceptor, tmpDir, DatasetService.class.getName(),
                // Referred to by string rather than Class.getName()
                // because DatasetStorageHandler and StreamStorageHandler
                // extend a Hive class, which isn't present in this class loader
                "co.cask.cdap.hive.datasets.DatasetStorageHandler", "co.cask.cdap.hive.stream.StreamStorageHandler",
                "org.apache.hadoop.hive.ql.exec.mr.ExecDriver", "org.apache.hive.service.cli.CLIService",
                "org.apache.hadoop.mapred.YarnClientProtocolProvider",
                // Needed for - at least - CDH 4.4 integration
                "org.apache.hive.builtins.BuiltinUtils",
                // Needed for - at least - CDH 5 integration
                "org.apache.hadoop.hive.shims.Hadoop23Shims"));
        orderedDependencies.addAll(additionalJars);

        exploreDependencies = orderedDependencies;
        return orderedDependencies;
    }

    /**
     * Trace the dependencies files of the given className, using the classLoader,
     * and including the classes that's accepted by the classAcceptor
     *
     * Nothing is returned if the classLoader, or if not provided, the ExploreRuntimeModule class loader,
     * does not contain the className.
     */
    public static Set<File> traceDependencies(@Nullable ClassLoader classLoader, final ClassAcceptor classAcceptor,
            File tmpDir, String... classNames) throws IOException {
        LOG.debug("Tracing dependencies for classes: {}", Arrays.toString(classNames));

        ClassLoader usingCL = classLoader;
        if (usingCL == null) {
            usingCL = ExploreRuntimeModule.class.getClassLoader();
        }

        final String rewritingClassName = HIVE_AUTHFACTORY_CLASS_NAME;
        final Set<File> rewritingFiles = Sets.newHashSet();

        final Set<File> jarFiles = Sets.newHashSet();
        Dependencies.findClassDependencies(usingCL, new ClassAcceptor() {
            @Override
            public boolean accept(String className, URL classUrl, URL classPathUrl) {
                if (!classAcceptor.accept(className, classUrl, classPathUrl)) {
                    return false;
                }

                if (rewritingClassName.equals(className)) {
                    rewritingFiles.add(new File(classPathUrl.getFile()));
                }

                jarFiles.add(new File(classPathUrl.getFile()));
                return true;
            }
        }, classNames);

        // Rewrite HiveAuthFactory.loginFromKeytab to be a no-op method.
        // This is needed because we don't want to use Hive's CLIService since
        // we're already using delegation tokens
        for (File rewritingFile : rewritingFiles) {
            // TODO: this may cause lots of rewrites since we may rewrite the same jar multiple times
            File rewrittenJar = rewriteHiveAuthFactory(rewritingFile,
                    new File(tmpDir, rewritingFile.getName() + "-" + System.currentTimeMillis() + ".jar"));
            jarFiles.add(rewrittenJar);
            LOG.debug("Rewrote {} to {}", rewritingFile.getAbsolutePath(), rewrittenJar.getAbsolutePath());
        }
        jarFiles.removeAll(rewritingFiles);

        if (LOG.isDebugEnabled()) {
            for (File jarFile : jarFiles) {
                LOG.debug("Added jar {}", jarFile.getAbsolutePath());
            }
        }

        return jarFiles;
    }

    @VisibleForTesting
    static File rewriteHiveAuthFactory(File sourceJar, File targetJar) throws IOException {
        try (JarFile input = new JarFile(sourceJar);
                JarOutputStream output = new JarOutputStream(new FileOutputStream(targetJar))) {
            String hiveAuthFactoryPath = HIVE_AUTHFACTORY_CLASS_NAME.replace('.', '/') + ".class";

            Enumeration<JarEntry> sourceEntries = input.entries();
            while (sourceEntries.hasMoreElements()) {
                JarEntry entry = sourceEntries.nextElement();
                output.putNextEntry(new JarEntry(entry.getName()));

                try (InputStream entryInputStream = input.getInputStream(entry)) {
                    if (!hiveAuthFactoryPath.equals(entry.getName())) {
                        ByteStreams.copy(entryInputStream, output);
                        continue;
                    }

                    try {
                        // Rewrite the bytecode of HiveAuthFactory.loginFromKeytab method to a no-op method
                        ClassReader cr = new ClassReader(entryInputStream);
                        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
                        cr.accept(new ClassVisitor(Opcodes.ASM5, cw) {
                            @Override
                            public MethodVisitor visitMethod(final int access, final String name, final String desc,
                                    String signature, String[] exceptions) {
                                MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature,
                                        exceptions);
                                if (!"loginFromKeytab".equals(name)) {
                                    return methodVisitor;
                                }
                                GeneratorAdapter adapter = new GeneratorAdapter(methodVisitor, access, name, desc);
                                adapter.returnValue();

                                // VisitMaxs with 0 so that COMPUTE_MAXS from ClassWriter will compute the right values.
                                adapter.visitMaxs(0, 0);
                                return new MethodVisitor(Opcodes.ASM5) {
                                };
                            }
                        }, 0);
                        output.write(cw.toByteArray());
                    } catch (Exception e) {
                        throw new IOException("Unable to generate HiveAuthFactory class", e);
                    }
                }
            }

            return targetJar;
        }
    }

    /**
     * Updates environment variables in hive-site.xml, mapred-site.xml and yarn-site.xml for explore.
     * All other conf files are returned without any update.
     * @param confFile conf file to update
     * @param tempDir temp dir to create files if necessary
     * @return the new conf file to use in place of confFile
     */
    public static File updateConfFileForExplore(File confFile, File tempDir) {
        if (HIVE_SITE_FILE_PATTERN.matcher(confFile.getAbsolutePath()).matches()) {
            return updateHiveConfFile(confFile, tempDir);
        } else if (YARN_SITE_FILE_PATTERN.matcher(confFile.getAbsolutePath()).matches()) {
            return updateYarnConfFile(confFile, tempDir);
        } else if (MAPRED_SITE_FILE_PATTERN.matcher(confFile.getAbsolutePath()).matches()) {
            return updateMapredConfFile(confFile, tempDir);
        } else {
            return confFile;
        }
    }

    /**
     * Change yarn-site.xml file, and return a temp copy of it to which are added
     * necessary options.
     */
    private static File updateYarnConfFile(File confFile, File tempDir) {
        Configuration conf = new Configuration(false);
        try {
            conf.addResource(confFile.toURI().toURL());
        } catch (MalformedURLException e) {
            LOG.error("File {} is malformed.", confFile, e);
            throw Throwables.propagate(e);
        }

        String yarnAppClassPath = conf.get(YarnConfiguration.YARN_APPLICATION_CLASSPATH,
                Joiner.on(",").join(YarnConfiguration.DEFAULT_YARN_APPLICATION_CLASSPATH));

        // add the pwd/* at the beginning of classpath. so user's jar will take precedence and without this change,
        // job.jar will be at the beginning of the classpath, since job.jar has old guava version classes,
        // we want to add pwd/* before
        yarnAppClassPath = "$PWD/*," + yarnAppClassPath;

        conf.set(YarnConfiguration.YARN_APPLICATION_CLASSPATH, yarnAppClassPath);

        File newYarnConfFile = new File(tempDir, "yarn-site.xml");
        try (FileOutputStream os = new FileOutputStream(newYarnConfFile)) {
            conf.writeXml(os);
        } catch (IOException e) {
            LOG.error("Problem creating and writing to temporary yarn-conf.xml conf file at {}", newYarnConfFile,
                    e);
            throw Throwables.propagate(e);
        }

        return newYarnConfFile;
    }

    /**
     * Change mapred-site.xml file, and return a temp copy of it to which are added
     * necessary options.
     */
    private static File updateMapredConfFile(File confFile, File tempDir) {
        Configuration conf = new Configuration(false);
        try {
            conf.addResource(confFile.toURI().toURL());
        } catch (MalformedURLException e) {
            LOG.error("File {} is malformed.", confFile, e);
            throw Throwables.propagate(e);
        }

        String mrAppClassPath = conf.get(MRJobConfig.MAPREDUCE_APPLICATION_CLASSPATH,
                MRJobConfig.DEFAULT_MAPREDUCE_APPLICATION_CLASSPATH);

        // Add the pwd/* at the beginning of classpath. Without this change, old jars from mr framework classpath
        // get into classpath.
        mrAppClassPath = "$PWD/*," + mrAppClassPath;

        conf.set(MRJobConfig.MAPREDUCE_APPLICATION_CLASSPATH, mrAppClassPath);

        File newMapredConfFile = new File(tempDir, "mapred-site.xml");
        try (FileOutputStream os = new FileOutputStream(newMapredConfFile)) {
            conf.writeXml(os);
        } catch (IOException e) {
            LOG.error("Problem creating and writing to temporary mapred-site.xml conf file at {}",
                    newMapredConfFile, e);
            throw Throwables.propagate(e);
        }

        return newMapredConfFile;
    }

    /**
     * Change hive-site.xml file, and return a temp copy of it to which are added
     * necessary options.
     */
    private static File updateHiveConfFile(File confFile, File tempDir) {
        Configuration conf = new Configuration(false);
        try {
            conf.addResource(confFile.toURI().toURL());
        } catch (MalformedURLException e) {
            LOG.error("File {} is malformed.", confFile, e);
            throw Throwables.propagate(e);
        }

        // we prefer jars at container's root directory before job.jar,
        // we edit the YARN_APPLICATION_CLASSPATH in yarn-site.xml using
        // co.cask.cdap.explore.service.ExploreServiceUtils.updateYarnConfFile and
        // setting the MAPREDUCE_JOB_CLASSLOADER and MAPREDUCE_JOB_USER_CLASSPATH_FIRST to false will put
        // YARN_APPLICATION_CLASSPATH before job.jar for container's classpath.
        conf.setBoolean(Job.MAPREDUCE_JOB_USER_CLASSPATH_FIRST, false);
        conf.setBoolean(MRJobConfig.MAPREDUCE_JOB_CLASSLOADER, false);

        String sparkHome = System.getenv(Constants.SPARK_HOME);
        if (sparkHome != null) {
            LOG.debug("Setting spark.home in hive conf to {}", sparkHome);
            conf.set("spark.home", sparkHome);
        }

        File newHiveConfFile = new File(tempDir, "hive-site.xml");

        try (FileOutputStream os = new FileOutputStream(newHiveConfFile)) {
            conf.writeXml(os);
        } catch (IOException e) {
            LOG.error("Problem creating temporary hive-site.xml conf file at {}", newHiveConfFile, e);
            throw Throwables.propagate(e);
        }
        return newHiveConfFile;
    }

    public static boolean isSparkAvailable() {
        try {
            // SparkUtils.locateSparkAssemblyJar() throws IllegalStateException if it is not able to locate spark jar
            SparkUtils.locateSparkAssemblyJar();
            return true;
        } catch (IllegalStateException e) {
            LOG.debug("Got exception while determining spark availability", e);
            return false;
        }
    }

    public static boolean isSparkEngine(HiveConf hiveConf) {
        // We don't support setting engine through session configuration now
        String engine = hiveConf.get("hive.execution.engine");
        return "spark".equalsIgnoreCase(engine);
    }

    // This method is used to determine if Tez is enabled based on TEZ_HOME environment variable.
    // Master prepares the explore container by adding jars available in TEZ_HOME.
    // However this environment variable is not available to explore container itself(BaseHiveService).
    // There we check the existence of tez using hive.execution.engine config variable.
    private static boolean isTezAvailable() {
        return System.getenv(Constants.TEZ_HOME) != null;
    }

    private static Set<File> getTezJars() {
        String tezHome = System.getenv(Constants.TEZ_HOME);
        Path tezHomeDir = Paths.get(tezHome);
        final PathMatcher pathMatcher = tezHomeDir.getFileSystem().getPathMatcher("glob:*.jar");
        final Set<File> tezJars = new HashSet<>();
        try {
            Files.walkFileTree(tezHomeDir, new SimpleFileVisitor<Path>() {
                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                    if (attrs.isRegularFile() && pathMatcher.matches(file.getFileName())) {
                        tezJars.add(file.toFile());
                    }
                    return FileVisitResult.CONTINUE;
                }

                @Override
                public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
                    // Ignore error
                    return FileVisitResult.CONTINUE;
                }
            });
        } catch (IOException e) {
            LOG.warn("Exception raised while inspecting {}", tezHomeDir, e);
        }
        return tezJars;
    }
}