org.sosy_lab.cpachecker.appengine.server.resource.TaskExecutorServerResource.java Source code

Java tutorial

Introduction

Here is the source code for org.sosy_lab.cpachecker.appengine.server.resource.TaskExecutorServerResource.java

Source

/*
 *  CPAchecker is a tool for configurable software verification.
 *  This file is part of CPAchecker.
 *
 *  Copyright (C) 2007-2014  Dirk Beyer
 *  All rights reserved.
 *
 *  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.
 *
 *
 *  CPAchecker web page:
 *    http://cpachecker.sosy-lab.org
 */
package org.sosy_lab.cpachecker.appengine.server.resource;

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.lang.Thread.UncaughtExceptionHandler;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.logging.Formatter;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;

import org.restlet.data.Form;
import org.restlet.engine.header.Header;
import org.restlet.ext.wadl.WadlServerResource;
import org.restlet.representation.Representation;
import org.restlet.util.Series;
import org.sosy_lab.common.concurrency.Threads;
import org.sosy_lab.common.configuration.Configuration;
import org.sosy_lab.common.configuration.ConfigurationBuilder;
import org.sosy_lab.common.configuration.ConfigurationBuilderFactory;
import org.sosy_lab.common.configuration.FileOption;
import org.sosy_lab.common.configuration.InvalidConfigurationException;
import org.sosy_lab.common.configuration.converters.FileTypeConverter;
import org.sosy_lab.common.io.Path;
import org.sosy_lab.common.io.Paths;
import org.sosy_lab.common.log.FileLogFormatter;
import org.sosy_lab.common.log.LogManager;
import org.sosy_lab.cpachecker.appengine.common.GAEConfigurationBuilder;
import org.sosy_lab.cpachecker.appengine.dao.TaskDAO;
import org.sosy_lab.cpachecker.appengine.dao.TaskFileDAO;
import org.sosy_lab.cpachecker.appengine.entity.Task;
import org.sosy_lab.cpachecker.appengine.entity.Task.Status;
import org.sosy_lab.cpachecker.appengine.io.GAEPathFactory;
import org.sosy_lab.cpachecker.appengine.log.GAELogHandler;
import org.sosy_lab.cpachecker.appengine.log.GAELogManager;
import org.sosy_lab.cpachecker.appengine.server.common.TaskExecutorResource;
import org.sosy_lab.cpachecker.appengine.util.DefaultOptions;
import org.sosy_lab.cpachecker.core.CPAchecker;
import org.sosy_lab.cpachecker.core.CPAcheckerResult;
import org.sosy_lab.cpachecker.core.ShutdownNotifier;
import org.sosy_lab.cpachecker.core.ShutdownNotifier.ShutdownRequestListener;
import org.sosy_lab.cpachecker.util.resources.ResourceLimitChecker;

import com.google.appengine.api.LifecycleManager;
import com.google.appengine.api.LifecycleManager.ShutdownHook;
import com.google.appengine.api.ThreadManager;
import com.google.apphosting.api.ApiProxy;
import com.google.common.io.FileWriteMode;

public class TaskExecutorServerResource extends WadlServerResource implements TaskExecutorResource {

    private Task task;
    private Level logLevel;
    private Path errorPath;
    private Configuration config;
    private LogManager logManager;
    private GAELogHandler logHandler;
    private Thread cpaCheckerThread;
    private volatile CPAcheckerResult result;

    private boolean configDumped = false;
    private boolean statsDumped = false;
    private boolean logDumped = false;

    private boolean shutdownComplete = false;

    @Override
    public void executeTask(Representation entity) throws Exception {
        Form requestValues = new Form(entity);
        task = TaskDAO.loadWithoutSanitizing(requestValues.getFirstValue("taskKey"));

        if (task == null) {
            return;
        }

        try {
            if (TaskFileDAO.loadByName(ERROR_FILE_NAME, task) != null || TaskFileDAO
                    .loadByName(DefaultOptions.getImmutableOptions().get("statistics.file"), task) != null) {
                return;
            }
        } catch (IOException e) {
            // continue business as usual
        }

        Threads.setThreadFactory(ThreadManager.currentRequestThreadFactory());
        GAEPathFactory.registerTaskWithCurrentThread(task);
        // TODO unregister task at some point to free memory?

        Paths.setFactory(new GAEPathFactory());
        errorPath = Paths.get(ERROR_FILE_NAME);

        @SuppressWarnings("unchecked")
        Series<Header> headers = (Series<Header>) getRequestAttributes().get("org.restlet.http.headers");
        int retries = Integer.parseInt(headers.getFirstValue("X-AppEngine-TaskRetryCount"));
        TaskDAO.reset(task); // clear for case of retry
        task.setRetries(retries);
        task.setRequestID((String) ApiProxy.getCurrentEnvironment().getAttributes()
                .get("com.google.appengine.runtime.request_log_id"));
        task.setExecutionDate(new Date());
        task.setStatus(Status.RUNNING);
        TaskDAO.save(task);

        buildConfiguration();
        setupLogging();
        dumpConfiguration();

        ShutdownRequestListener listener = new ShutdownRequestListener() {

            @Override
            public void shutdownRequested(final String reason) {
                log(Level.WARNING, "Task timed out. Trying to rescue results.", reason);

                try {
                    cpaCheckerThread.join(10000); // 10 seconds
                } catch (Exception e) {
                    // Never mind. We are shutting down and only want to do so gracefully.
                }

                /*
                 * Sometimes there are weird race conditions going on and saving might
                 * fail therefore.In this case we try again a couple of times.
                 */
                int retries = 0;
                do {
                    try {
                        setResult();
                        task.setStatus(Status.TIMEOUT);
                        task.setTerminationDate(new Date());
                        task.setStatusMessage(reason);
                        TaskDAO.save(task);

                        dumpStatistics();
                        dumpLog();
                        break;
                    } catch (Exception e) {
                        log(Level.WARNING, "Error while trying to rescue results.", e);
                        retries++;
                    }
                } while (retries < 3);

                shutdownComplete = true;
            }
        };

        final ShutdownNotifier shutdownNotifier = ShutdownNotifier.create();
        shutdownNotifier.register(listener);

        ShutdownHook shutdownHook = new ShutdownHook() {
            @Override
            public void shutdown() {
                shutdownNotifier.requestShutdown("The backend is shutting down.");
            }
        };
        LifecycleManager.getInstance().setShutdownHook(shutdownHook);

        final CPAchecker cpaChecker = new CPAchecker(config, logManager, shutdownNotifier);

        /*
         * To prevent the main thread (and therefore the complete request) to go
         * down if the run is interrupted the checker is run in its own thread. This
         * allows for setting the task's status and potentially saving results.
         */
        cpaCheckerThread = Threads.newThread(new Runnable() {

            @Override
            public void run() {
                try {
                    result = cpaChecker.run(task.getProgram().getPath());
                } catch (Exception e) {
                    if (e.getClass().getSimpleName().equals("RuntimeException")) {
                        /* RuntimeException might be thrown if the thread is interrupted and
                         * data store operations are going on. The exception needs to be
                         * ignored to be able to bail out sensible.
                         */
                        log(Level.WARNING, e);
                    } else {
                        log(Level.WARNING, e);
                        throw new IllegalStateException(e);
                    }
                }
            }
        });

        UncaughtExceptionHandler handler = new UncaughtExceptionHandler() {

            @Override
            public void uncaughtException(Thread t, Throwable e) {
                doCatch(e);
            }
        };
        cpaCheckerThread.setUncaughtExceptionHandler(handler);

        ResourceLimitChecker limits = ResourceLimitChecker.fromConfiguration(config, logManager, shutdownNotifier);
        limits.start();

        cpaCheckerThread.start();
        cpaCheckerThread.join();

        if (shutdownNotifier.shouldShutdown()) {
            while (!shutdownComplete) {
                Thread.sleep(100);
            }
        } else {
            shutdownNotifier.unregister(listener);
            limits.cancel();

            // do not overwrite any previous status
            if (task.getStatus() != Status.ERROR && task.getStatus() != Status.TIMEOUT) {
                dumpStatistics();
                dumpLog();

                setResult();
                task.setTerminationDate(new Date());
                task.setStatus(Status.DONE);
                TaskDAO.save(task);
            }
        }
    }

    /*
     * Catches all exceptions that are thrown while running and are not
     * handled along the way.
     *
     * @see org.restlet.resource.ServerResource#doCatch(java.lang.Throwable)
     */
    @Override
    protected void doCatch(Throwable e) {
        // set status OK to pretend everything went fine so that the task will not be re-tried.
        getResponse().setStatus(org.restlet.data.Status.SUCCESS_OK);

        Throwable originalThrowable = e;

        if (e.getCause() != null) {
            e = e.getCause();
        }

        String message = (e.getMessage() == null) ? "" : e.getMessage();

        switch (e.getClass().getSimpleName()) {
        case "DeadlineExceededException":
            task.setStatus(Status.TIMEOUT);
            task.setStatusMessage("The task timed out. Results may be available however.");
            log(Level.WARNING, "Task timed out. Trying to rescue results.", e);
            break;
        case "InvalidConfigurationException":
            task.setStatusMessage("The given configuration is invalid.");
            log(Level.WARNING, "The given configuration is invalid.", e);
            break;
        case "IOException":
            task.setStatusMessage(String.format("An I/O error occurred: %s", message));
            log(Level.WARNING, "An I/O error occurred.", e);
            break;
        default:
            task.setStatusMessage(String.format("An error occured: %s", message));
            log(Level.WARNING, "There was an error", e);
        }

        if (task.getStatus() != Status.TIMEOUT) {
            task.setStatus(Status.ERROR);
        }

        try {
            saveStackTrace(originalThrowable);
            setResult();
            task.setTerminationDate(new Date());
            TaskDAO.save(task);

            dumpConfiguration();
            dumpStatistics();
            dumpLog();
        } catch (IOException _) {
            // we are already in an error state so ignore any further one
        }
    }

    private void log(Level level, Object... args) {
        if (logManager != null) {
            logManager.log(level, args);
        }
    }

    private void setResult() {
        if (result != null) {
            task.setResultMessage(result.getResultString());
            task.setResultOutcome(result.getResult());
        }
    }

    private void saveStackTrace(Throwable e) throws IOException {
        try (OutputStream out = errorPath.asByteSink(FileWriteMode.APPEND).openStream()) {
            PrintStream ps = new PrintStream(out);
            e.printStackTrace(ps);
            ps.flush();
        }
    }

    private void dumpLog() {
        if (logDumped) {
            return;
        }

        if (logHandler != null && logLevel != null && logLevel != Level.OFF) {
            logDumped = true;
            logHandler.flushAndClose();
        }
    }

    private void dumpConfiguration() throws IOException {
        if (configDumped) {
            return;
        }

        if (config != null && config.getProperty("configuration.dumpFile") != null
                && !config.getProperty("configuration.dumpFile").equals("")) {
            Path configurationDumpFile = Paths.get(config.getProperty("configuration.dumpFile"));
            if (configurationDumpFile != null) {
                configurationDumpFile.asCharSink(StandardCharsets.UTF_8).write(config.asPropertiesString());
                configDumped = true;
            }
        }
    }

    private void dumpStatistics() throws IOException {
        if (statsDumped) {
            return;
        }

        if (config == null || config.getProperty("statistics.export").equals("false") || result == null) {
            return;
        }

        Path statisticsDumpFile = Paths.get(config.getProperty("statistics.file"));
        try (OutputStream out = statisticsDumpFile.asByteSink().openBufferedStream()) {
            PrintStream stream = new PrintStream(out);
            result.printStatistics(stream);
            stream.println();

            if (result != null) {
                result.printResult(stream);
            }

            stream.flush();
            statsDumped = true;
        }
    }

    private void setupLogging() throws IOException, InvalidConfigurationException {
        logLevel = Level.parse(config.getProperty("log.level"));

        if (logLevel != Level.OFF) {
            Formatter fileLogFormatter = new FileLogFormatter();
            OutputStream logFileStream = Paths.get("CPALog.txt").asByteSink().openBufferedStream();
            logHandler = new GAELogHandler(logFileStream, fileLogFormatter, logLevel);
            logManager = new GAELogManager(config, new DummyHandler(), logHandler);
        } else {
            logManager = new GAELogManager(config, new DummyHandler(), new DummyHandler());
        }
    }

    private void buildConfiguration() throws IOException, InvalidConfigurationException {
        Configuration.setBuilderFactory(new ConfigurationBuilderFactory() {
            @Override
            public ConfigurationBuilder getBuilder() {
                return new GAEConfigurationBuilder();
            }
        });

        ConfigurationBuilder configurationBuilder = Configuration.builder();
        configurationBuilder.setOptions(DefaultOptions.getDefaultOptions());
        if (task.getConfiguration() != null) {
            configurationBuilder
                    .loadFromFile(Paths.get(DefaultOptions.CONFIGURATIONS_DIR, task.getConfiguration()));
        }
        configurationBuilder.setOptions(task.getOptions());

        if (task.getSpecification() != null) {
            configurationBuilder.setOption("specification",
                    DefaultOptions.SPECIFICATIONS_DIR + "/" + task.getSpecification());
        }

        Configuration configuration = configurationBuilder.build();

        FileTypeConverter fileTypeConverter = FileTypeConverter.createWithSafePathsOnly(configuration);

        config = Configuration.builder().copyFrom(configuration).addConverter(FileOption.class, fileTypeConverter)
                .build();

        Configuration.getDefaultConverters().put(FileOption.class, fileTypeConverter);
    }

    private static class DummyHandler extends Handler {

        @Override
        public void publish(LogRecord pRecord) {
        }

        @Override
        public void flush() {
        }

        @Override
        public void close() throws SecurityException {
        }
    }
}