Package com.asakusafw.runtime.stage.launcher

Source Code of com.asakusafw.runtime.stage.launcher.LauncherOptionsParser

/**
* Copyright 2011-2014 Asakusa Framework Team.
*
* 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 com.asakusafw.runtime.stage.launcher;

import static com.asakusafw.runtime.stage.launcher.Util.*;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.zip.ZipFile;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.LocalFileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.util.GenericOptionsParser;
import org.apache.hadoop.util.Tool;

import com.asakusafw.runtime.stage.StageConstants;
import com.asakusafw.runtime.util.cache.BatchFileCacheRepository;
import com.asakusafw.runtime.util.cache.ConcurrentBatchFileCacheRepository;
import com.asakusafw.runtime.util.cache.FileCacheRepository;
import com.asakusafw.runtime.util.cache.HadoopFileCacheRepository;
import com.asakusafw.runtime.util.cache.NullBatchFileCacheRepository;
import com.asakusafw.runtime.util.lock.ConstantRetryStrategy;
import com.asakusafw.runtime.util.lock.LocalFileLockProvider;

/**
* Converts application arguments into {@link LauncherOptions}.
* @since 0.7.0
*/
public final class LauncherOptionsParser {

    static final Log LOG = LogFactory.getLog(LauncherOptionsParser.class);

    private static final ThreadFactory DAEMON_THREAD_FACTORY = new ThreadFactory() {
        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r);
            t.setDaemon(true);
            return t;
        }
    };

    private static final String SYSPROP_TMPDIR = "java.io.tmpdir";

    private static final String SYSPROP_USER_NAME = "user.name";

    private static final String PATTERN_TMPDIR_NAME = "asakusa-launcher-cache-{0}";

    static final String KEY_ARG_LIBRARIES = "-libjars";

    static final String KEY_CONF_LIBRARIES = "tmpjars";

    static final String KEY_CONF_JAR = StageConstants.PROP_APPLICATION_JAR;

    /**
     * The configuration key of whether library cache mechanism is enabled or not.
     */
    public static final String KEY_CACHE_ENABLED = "com.asakusafw.launcher.cache.enabled";

    /**
     * The configuration key of remote cache repository path.
     */
    public static final String KEY_CACHE_REPOSITORY = "com.asakusafw.launcher.cache.path";

    /**
     * The configuration key of max number of cache building retry attempt.
     */
    public static final String KEY_CACHE_RETRY_COUNT = "com.asakusafw.launcher.cache.retry.max";

    /**
     * The configuration key of interval long of cache building retry attempt.
     */
    public static final String KEY_CACHE_RETRY_INTERVAL = "com.asakusafw.launcher.cache.retry.interval";

    /**
     * The configuration key of local temporary directory for cache building.
     */
    public static final String KEY_CACHE_TEMPORARY = "com.asakusafw.launcher.cache.local";

    /**
     * The configuration key of max thread number for cache building.
     */
    public static final String KEY_MAX_THREADS = "com.asakusafw.launcher.cache.threads";

    /**
     * The configuration key of whether cached job jar is enabled or not.
     */
    public static final String KEY_CACHE_JOBJAR = "com.asakusafw.launcher.cache.jobjar";

    static final String PATH_LOCK_DIRECTORY = "lock";

    static final boolean DEFAULT_CACHE_ENABLED = true;

    static final int DEFAULT_CACHE_RETRY_COUNT = 50;

    static final long DEFAULT_CACHE_RETRY_INTERVAL = 100;

    static final boolean DEFAULT_CACHE_JOBJAR = true;

    static final int MINIMUM_MAX_THREADS = 1;

    static final int DEFAULT_MAX_THREADS = 4;

    private final Configuration configuration;

    private final List<String> arguments;

    private final Set<Object> applicationResources = new HashSet<Object>();

    private final Set<File> applicationCacheFiles = new HashSet<File>();

    private LauncherOptionsParser(Configuration configuration, String[] args) {
        this.configuration = configuration;
        this.arguments = Arrays.asList(args);
    }

    /**
     * Analyze application arguments and returns {@link LauncherOptions} object.
     * @param configuration the Hadoop configuration for the application
     * @param args the launcher arguments
     * @return the analyzed options
     * @throws IllegalArgumentException if launch arguments are not valid
     * @throws IOException if failed to build launch options by I/O error
     * @throws InterruptedException if interrupted while building launch options
     */
    public static LauncherOptions parse(
            Configuration configuration,
            String... args) throws IOException, InterruptedException {
        LauncherOptionsParser parser = new LauncherOptionsParser(configuration, args);
        boolean success = false;
        try {
            LauncherOptions result = parser.analyze();
            success = true;
            return result;
        } finally {
            parser.cleanUp(success);
        }
    }

    private LauncherOptions analyze() throws IOException, InterruptedException {
        LinkedList<String> copy = new LinkedList<String>(arguments);
        String applicationClassName = consumeApplicationClassName(copy);
        List<Path> libraryPaths = consumeLibraryPaths(copy);
        GenericOptionsParser genericOptions = processGenericOptions(copy);
        URLClassLoader applicationClassLoader = buildApplicationClassLoader(libraryPaths, applicationClassName);
        Class<? extends Tool> applicationClass = buildApplicationClass(applicationClassName);

        return new LauncherOptions(
                configuration,
                applicationClass,
                Arrays.asList(genericOptions.getRemainingArgs()),
                applicationClassLoader,
                applicationCacheFiles);
    }

    private String consumeApplicationClassName(LinkedList<String> rest) {
        if (rest.isEmpty()) {
            throw new IllegalArgumentException("the first argument must be target application class name");
        }
        return rest.removeFirst();
    }

    private List<Path> consumeLibraryPaths(LinkedList<String> rest) throws IOException {
        List<String> names = consumeLibraryNames(rest);
        if (names.isEmpty()) {
            return Collections.emptyList();
        }
        List<Path> results = new ArrayList<Path>();
        LocalFileSystem local = FileSystem.getLocal(configuration);
        for (String name : names) {
            Path path = new Path(name);
            FileSystem fs;
            if (path.toUri().getScheme() == null) {
                fs = local;
            } else {
                fs = path.getFileSystem(configuration);
            }
            path = fs.makeQualified(path);
            if (fs.exists(path) == false) {
                throw new FileNotFoundException(path.toString());
            }
            results.add(path);
        }
        return results;
    }

    private List<String> consumeLibraryNames(LinkedList<String> rest) {
        List<String> results = new ArrayList<String>();
        for (Iterator<String> iter = rest.iterator(); iter.hasNext();) {
            String token = iter.next();
            if (token.equals(KEY_ARG_LIBRARIES)) {
                iter.remove();
                if (iter.hasNext()) {
                    String libraries = iter.next();
                    iter.remove();
                    for (String library : libraries.split(",")) {
                        String path = library.trim();
                        if (path.isEmpty() == false) {
                            results.add(path);
                        }
                    }
                }
            }
        }
        return results;
    }

    private GenericOptionsParser processGenericOptions(LinkedList<String> rest) throws IOException {
        String[] args = rest.toArray(new String[rest.size()]);
        return new GenericOptionsParser(configuration, args);
    }

    private URLClassLoader buildApplicationClassLoader(
            List<Path> libraryPaths,
            String applicationClassName) throws IOException, InterruptedException {
        final List<URL> libraries = processLibraries(libraryPaths, applicationClassName);
        final ClassLoader parent = configuration.getClassLoader();
        URLClassLoader application = AccessController.doPrivileged(new PrivilegedAction<URLClassLoader>() {
            @Override
            public URLClassLoader run() {
                return new URLClassLoader(libraries.toArray(new URL[libraries.size()]), parent);
            }
        });
        this.applicationResources.add(application);
        configuration.setClassLoader(application);
        return application;
    }

    private List<URL> processLibraries(
            List<Path> libraryPaths,
            String applicationClassName) throws IOException, InterruptedException {
        if (libraryPaths.isEmpty()) {
            return Collections.emptyList();
        }
        Map<Path, Path> resolved = processLibraryCache(libraryPaths);
        List<URL> localUrls = new ArrayList<URL>();
        List<Path> remotePaths = new ArrayList<Path>();
        for (Path path : libraryPaths) {
            URI uri = path.toUri();
            assert uri.getScheme() != null;
            if (uri.getScheme().equals("file")) {
                localUrls.add(uri.toURL());
            }
            Path remote = resolved.get(path);
            if (remote == null) {
                remotePaths.add(path);
            } else {
                remotePaths.add(remote);
            }
        }

        if (configuration.getBoolean(KEY_CACHE_JOBJAR, DEFAULT_CACHE_JOBJAR)) {
            configureJobJar(libraryPaths, applicationClassName, resolved);
        }

        String libjars = buildLibjars(remotePaths);
        if (libjars.isEmpty() == false) {
            configuration.set(KEY_CONF_LIBRARIES, libjars);
        }
        return localUrls;
    }

    private void configureJobJar(List<Path> paths, String className, Map<Path, Path> cacheMap) throws IOException {
        if (configuration.get(KEY_CONF_JAR) != null) {
            return;
        }
        for (Path path : paths) {
            Path remote = cacheMap.get(path);
            URI uri = path.toUri();
            if (remote != null && uri.getScheme().equals("file")) {
                File file = new File(uri);
                if (isInclude(file, className)) {
                    Path qualified = remote.getFileSystem(configuration).makeQualified(remote);
                    if (LOG.isDebugEnabled()) {
                        LOG.debug(MessageFormat.format(
                                "Application class is in: file={2} ({1}), class={0}",
                                className,
                                path,
                                qualified));
                    }
                    URI target = qualified.toUri();
                    if (target.getScheme() != null
                            && (target.getScheme().equals("file") || target.getAuthority() != null)) {
                        configuration.set(KEY_CONF_JAR, qualified.toString());
                    }
                    break;
                }
            }
        }
    }

    private boolean isInclude(File file, String className) {
        try {
            ZipFile zip = new ZipFile(file);
            try {
                return zip.getEntry(className.replace('.', '/') + ".class") != null;
            } finally {
                zip.close();
            }
        } catch (Exception e) {
            if (LOG.isDebugEnabled()) {
                LOG.debug(MessageFormat.format(
                        "Exception occurred while detecting for class: file={0}, class={1}",
                        file,
                        className), e);
            }
        }
        return false;
    }

    private String buildLibjars(List<Path> paths) throws IOException {
        StringBuilder buf = new StringBuilder(configuration.get(KEY_CONF_LIBRARIES, ""));
        for (Path path : paths) {
            if (buf.length() != 0) {
                buf.append(',');
            }
            FileSystem fs = path.getFileSystem(configuration);
            buf.append(fs.makeQualified(path).toString());
        }
        String libjars = buf.toString();
        return libjars;
    }

    private Map<Path, Path> processLibraryCache(List<Path> libraryPaths) throws IOException, InterruptedException {
        boolean useCache = computeEnabled();
        if (useCache) {
            Path repositoryPath = computeRepositoryPath();
            File temporary = computeTemporaryDirectory();
            int threads = Math.max(configuration.getInt(KEY_MAX_THREADS, DEFAULT_MAX_THREADS), MINIMUM_MAX_THREADS);
            int retryCount = configuration.getInt(KEY_CACHE_RETRY_COUNT, DEFAULT_CACHE_RETRY_COUNT);
            long retryInterval = configuration.getLong(KEY_CACHE_RETRY_INTERVAL, DEFAULT_CACHE_RETRY_INTERVAL);
            FileCacheRepository unit = new HadoopFileCacheRepository(
                    configuration,
                    repositoryPath,
                    new LocalFileLockProvider<Path>(new File(temporary, PATH_LOCK_DIRECTORY)),
                    new ConstantRetryStrategy(retryCount, retryInterval));
            ExecutorService executor = Executors.newFixedThreadPool(threads, DAEMON_THREAD_FACTORY);
            try {
                BatchFileCacheRepository repo = new ConcurrentBatchFileCacheRepository(unit, executor);
                return repo.resolve(libraryPaths);
            } finally {
                executor.shutdownNow();
            }
        } else {
            return new NullBatchFileCacheRepository().resolve(libraryPaths);
        }
    }

    private boolean computeEnabled() {
        boolean explicit = configuration.getBoolean(KEY_CACHE_ENABLED, DEFAULT_CACHE_ENABLED);
        if (explicit == false) {
            return false;
        }
        String repositoryPath = configuration.get(KEY_CACHE_REPOSITORY);
        if (repositoryPath == null || repositoryPath.trim().isEmpty()) {
            return false;
        }
        return true;
    }

    private Path computeRepositoryPath() throws IOException {
        assert configuration.get(KEY_CACHE_REPOSITORY) != null;
        Path repositoryPath = new Path(configuration.get(KEY_CACHE_REPOSITORY));
        repositoryPath = repositoryPath.getFileSystem(configuration).makeQualified(repositoryPath);
        if (LOG.isDebugEnabled()) {
            LOG.debug(MessageFormat.format(
                    "Using cache repository: -D{0}={1}",
                    KEY_CACHE_REPOSITORY,
                    repositoryPath));
        }
        return repositoryPath;
    }

    private File computeTemporaryDirectory() {
        String temporary = configuration.get(KEY_CACHE_TEMPORARY);
        if (temporary != null) {
            return new File(temporary);
        }
        String tmpdir = System.getProperty(SYSPROP_TMPDIR);
        if (tmpdir == null) {
            throw new IllegalStateException(MessageFormat.format(
                    "System property \"{0}\" must be defined",
                    SYSPROP_TMPDIR));
        }
        String name = System.getProperty(SYSPROP_USER_NAME);
        if (name == null) {
            throw new IllegalStateException(MessageFormat.format(
                    "System property \"{0}\" must be defined",
                    SYSPROP_USER_NAME));
        }
        String folder = MessageFormat.format(PATTERN_TMPDIR_NAME, name);
        File result = new File(tmpdir, folder);
        if (LOG.isDebugEnabled()) {
            LOG.debug(MessageFormat.format(
                    "Auto configuration: -D{0}={1}",
                    KEY_CACHE_TEMPORARY,
                    result));
        }
        return result;
    }

    private Class<? extends Tool> buildApplicationClass(String applicationClassName) {
        Class<? extends Tool> applicationClass;
        try {
            Class<?> aClass = configuration.getClassByName(applicationClassName);
            if (Tool.class.isAssignableFrom(aClass) == false) {
                throw new IllegalArgumentException(MessageFormat.format(
                        "Application \"{0}\" must be a subclass of \"{1}\"",
                        aClass.getName(),
                        Tool.class.getName()));
            }
            applicationClass = aClass.asSubclass(Tool.class);
        } catch (ClassNotFoundException e) {
            throw new IllegalArgumentException(MessageFormat.format(
                    "Application \"{0}\" is not found",
                    applicationClassName));
        }
        return applicationClass;
    }

    private void cleanUp(boolean success) {
        if (success == false) {
            for (Object object : applicationResources) {
                closeQuiet(object);
            }
            for (File file : applicationCacheFiles) {
                if (delete(file) == false) {
                    LOG.warn(MessageFormat.format(
                            "Failed to delete the application cache directory: {0}",
                            file));
                }
            }
        }
    }
}
TOP

Related Classes of com.asakusafw.runtime.stage.launcher.LauncherOptionsParser

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.