models.Application.java Source code

Java tutorial

Introduction

Here is the source code for models.Application.java

Source

/*
 * Copyright 2011 Matthias van der Vlies
 *
 *  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 models;

import java.io.File;
import java.net.ConnectException;
import java.util.Set;
import java.util.concurrent.TimeoutException;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.Transient;

import org.apache.commons.io.FileUtils;

import play.Logger;
import play.Play;
import play.Play.Mode;
import play.data.validation.Match;
import play.data.validation.Required;
import play.db.jpa.Model;
import play.libs.WS;
import play.libs.WS.WSRequest;
import scm.VersionControlSystem;
import scm.VersionControlSystemFactory;
import scm.VersionControlSystemFactory.VersionControlSystemType;
import core.ConfigurationManager;
import core.ProcessManager;
import core.ProcessManager.ProcessType;

/**
 * JPA entity for defining an application
 */
@Entity
@Table(name = "applications")
public class Application extends Model {

    private static final int ONE_SECOND = 1000;

    /**
     * Program ID
     */
    @Required
    @Column(updatable = false, unique = true, nullable = false)
    public String pid;

    /**
     * Type of VCS used for checkout
     */
    @Column(updatable = false, nullable = false)
    @Required
    public VersionControlSystemType vcsType;

    /**
     * URL to be used for the VCS
     */
    @Column(updatable = false, nullable = false)
    @Required
    public String vcsUrl;

    /**
     * Is the application checked out by the container?
     */
    public Boolean checkedOut = false;

    /**
     * Is the application enabled? i.e. started/stopped
     */
    public Boolean enabled;

    /**
     * What Play! mode should be used for running the application
     */
    @Column(updatable = true, nullable = false)
    @Required
    public Mode mode;

    /**
     * App subfolder, should always end with /
     */
    @Column(nullable = true)
    @Match("^.*/$")
    public String subfolder;

    /**
     * Configuration properties used for application.conf generation
     */
    @OneToMany(fetch = FetchType.EAGER, mappedBy = "application")
    public Set<ApplicationProperty> properties;

    public static Integer getCommandTimeout() {
        return Integer.valueOf(Play.configuration.getProperty("command.timeout"));
    }

    /**
     * Start the application
     * @param force Force start?
     * @param enable Set application enabled
     */
    public void start(boolean force, boolean enable) throws Exception {
        if (!force && !enabled) {
            throw new Exception("Can not start disabled application " + pid);
        } else if (isRunning()) {
            throw new Exception("Application " + pid + " is already running.");
        }

        // generate application.conf
        ConfigurationManager.generateConfigurationFiles(this);

        // Store play start pid for kept pid process
        final String startPid = pid + ProcessManager.PROCESS_START_POSTFIX;

        try {
            // Some processes may take some time to boot (pre-compiling, @OnApplicationStart jobs)
            // So we will be making some HTTP requests to check if it's up
            final ApplicationProperty address = ApplicationProperty.findHostProperty(this);
            final ApplicationProperty port = ApplicationProperty.findPortProperty(this);
            final String url = "http://" + (address == null ? "127.0.0.1" : address.value) + ":" + port.value;

            // Let's first see if there already is another application running on this port
            checkForOtherApplication(url);

            ProcessManager.executeCommand(startPid, ProcessManager.getFullPlayPath() + " start .",
                    new StringBuffer(), new File("apps/" + pid + "/" + (subfolder == null ? "" : subfolder)), true);

            // Send 'ping' HTTP requests to verify the application
            checkApplicationIsRunning(url);

            // final check just to make sure it really started
            ProcessManager.executeCommand(pid + "-status", ProcessManager.getFullPlayPath() + " status .",
                    new StringBuffer(), new File("apps/" + pid + "/" + (subfolder == null ? "" : subfolder)),
                    false);

            if (enable) {
                enabled = true;
                save();

                // flush the state to the database because we are going to remove the kept id
                em().flush();
            }

            Logger.info("Started %s", pid);
        } catch (TimeoutException e) {
            Logger.info("Could not determine whether %s started, time-out value: %s reached", pid,
                    getCommandTimeout());
            Logger.info("Check status manually and remove server.pid manually when needed");
            throw e;
        } catch (Exception e) {
            Logger.info(e, "Failed to start %s", pid);

            // Try to delete server.pid
            final File serverPid = new File(
                    "apps/" + pid + "/" + (subfolder == null ? "" : subfolder) + "server.pid");
            if (force && serverPid.exists()
                    && !new File("apps/" + pid + "/" + (subfolder == null ? "" : subfolder) + "server.pid")
                            .delete()) {
                throw new Exception("Unable to remove server.pid for falsely started application, remove manually");
            }

            throw e;
        } finally {
            ProcessManager.removeKeptPid(startPid);
        }
    }

    /**
     * Verify whether an application is started by sending HTTP 'pings' for a specified time-out period
     * @throws TimeoutException Is thrown when the time-out period is expired
     */
    private void checkApplicationIsRunning(final String url) throws InterruptedException, TimeoutException {
        int n = 0;
        int timeout = getCommandTimeout();
        while (n < timeout) {
            try {
                final WSRequest request = WS.url(url);
                request.timeout("1s"); // low time-out so we make sure the time-out cycle is as long as we define it to be
                request.get();
                break;
            } catch (RuntimeException e) {
                Thread.sleep(ONE_SECOND);
                n++;
            }
        }

        if (n == timeout) {
            throw new TimeoutException("Time-out value reached");
        }
    }

    /**
     * Check for any other application that may be present on the port and throw an exception if there is any.
     */
    private void checkForOtherApplication(final String url) throws Exception {
        try {
            final WSRequest request = WS.url(url);
            request.timeout("1s"); // set time-out to a low value to make sure
            // we time-out when there is a connect but
            // no answer, for example with SSH
            request.get();

            throw new Exception("There is already another application bound to " + url);
        } catch (Exception e) {
            // very dirty, but Play! wraps all upper level exceptions, so there really isn't any other way
            if (e.getCause() != null && e.getCause().getCause() != null
                    && e.getCause().getCause() instanceof ConnectException) {
                // this is good
                Logger.info("Port seems to be free");
            } else {
                // this means that there is an application there 
                // there is either a timeout, a HTTP app, or another protocol than HTTP
                throw new Exception("There is already another application bound to " + url + ": " + e.getMessage());
            }
        }
    }

    /**
     * Run play deps command for the application
     */
    private void resolveDependencies() throws Exception {
        final String command = ProcessManager.getFullPlayPath() + " deps --sync "
                + (mode == Mode.PROD ? "--forProd" : "") + " .";

        ProcessManager.executeCommand(pid + "-deps", command, new StringBuffer(),
                new File("apps/" + pid + "/" + (subfolder == null ? "" : subfolder)), false);
    }

    /**
     * Stop the application
     */
    public void stop() throws Exception {
        ProcessManager.executeProcess(pid + "-stop", ProcessManager.getFullPlayPath() + " stop .",
                new File("apps/" + pid + "/" + (subfolder == null ? "" : subfolder)), false);
        Logger.info("Application %s stopped", pid);
    }

    /**
     * Restart the application
     */
    public void restart() throws Exception {
        // if the application still has enabled as true a stop will kill the process and the process manager will restart it
        stop();
    }

    /**
     * Is the application running?
     */
    @Transient
    public boolean isRunning() throws Exception {
        if (!checkedOut) {
            throw new Exception("Application " + pid + " has not yet been checked out from SCM");
        }
        return ProcessManager.isProcessRunning(pid + "/" + (subfolder == null ? "" : subfolder), ProcessType.PLAY);
    }

    public boolean isBooting() throws Exception {
        return ProcessManager.isKeptPidAvailable(pid + ProcessManager.PROCESS_START_POSTFIX);
    }

    /**
     * Pull most recent version from VCS
     */
    public void pull() throws Exception {
        // pull before touching the process (or we risk killing a process on updating failure)
        final VersionControlSystem vcs = VersionControlSystemFactory.getVersionControlSystem(vcsType);
        vcs.cleanup(this); // cleanup working directory
        vcs.update(pid); // pull changes from git

        resolveDependencies();

        // if the application was already running this will force the process manager to restart the process
        stop();
    }

    /**
     * Fetch application from SCM for the first time
     */
    public void checkout() throws Exception {
        if (checkedOut) {
            throw new Exception("Application " + pid + " is already checked out");
        }

        VersionControlSystemFactory.getVersionControlSystem(vcsType).checkout(pid, vcsUrl);

        resolveDependencies();

        checkedOut = true;
        save();

        ConfigurationManager.readCurrentConfigurationFromFile(this);
    }

    /**
     * Removes checkout after deleting an application
     */
    public void clean() throws Exception {
        Logger.info("Removing SCM checkout for %s", pid);

        try {
            stop();
        } catch (Exception e) {
            // ignore
        }

        FileUtils.deleteDirectory(new File("apps/" + pid));
    }

    /**
     * Run the 'play status' command for this application and return its output
     */
    public synchronized String status() throws Exception {
        return ProcessManager.executeCommand("status-" + pid, ProcessManager.getFullPlayPath() + " status .",
                new StringBuffer(), false, new File("apps/" + pid + "/" + (subfolder == null ? "" : subfolder)),
                false);
    }
}