All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.mule.module.launcher.MuleDeploymentService Maven / Gradle / Ivy

/*
 * $Id$
 * --------------------------------------------------------------------------------------
 * Copyright (c) MuleSoft, Inc.  All rights reserved.  http://www.mulesoft.com
 *
 * The software in this package is published under the terms of the CPAL v1.0
 * license, a copy of which has been included with this distribution in the
 * LICENSE.txt file.
 */

package org.mule.module.launcher;

import static org.mule.util.SplashScreen.miniSplash;
import org.mule.config.StartupContext;
import org.mule.config.i18n.MessageFactory;
import org.mule.module.launcher.application.Application;
import org.mule.module.launcher.application.ApplicationClassLoaderFactory;
import org.mule.module.launcher.application.ApplicationFactory;
import org.mule.module.launcher.application.CompositeApplicationClassLoaderFactory;
import org.mule.module.launcher.application.DefaultApplicationFactory;
import org.mule.module.launcher.application.MuleApplicationClassLoaderFactory;
import org.mule.module.launcher.util.DebuggableReentrantLock;
import org.mule.module.launcher.util.ElementAddedEvent;
import org.mule.module.launcher.util.ElementRemovedEvent;
import org.mule.module.launcher.util.ObservableList;
import org.mule.module.reboot.MuleContainerBootstrapUtils;
import org.mule.util.ArrayUtils;
import org.mule.util.CollectionUtils;
import org.mule.util.FileUtils;
import org.mule.util.StringUtils;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.commons.beanutils.BeanPropertyValueEqualsPredicate;
import org.apache.commons.beanutils.BeanToPropertyValueTransformer;
import org.apache.commons.io.filefilter.AndFileFilter;
import org.apache.commons.io.filefilter.DirectoryFileFilter;
import org.apache.commons.io.filefilter.FileFileFilter;
import org.apache.commons.io.filefilter.IOFileFilter;
import org.apache.commons.io.filefilter.SuffixFileFilter;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class MuleDeploymentService implements DeploymentService
{
    public static final String APP_ANCHOR_SUFFIX = "-anchor.txt";
    public static final String ZIP_FILE_SUFFIX = ".zip";
    public static final IOFileFilter ZIP_APPS_FILTER = new AndFileFilter(new SuffixFileFilter(ZIP_FILE_SUFFIX), FileFileFilter.FILE);

    public static final String ANOTHER_DEPLOYMENT_OPERATION_IS_IN_PROGRESS = "Another deployment operation is in progress";
    public static final String INSTALL_OPERATION_HAS_BEEN_INTERRUPTED = "Install operation has been interrupted";

    protected static final int DEFAULT_CHANGES_CHECK_INTERVAL_MS = 5000;
    public static final String CHANGE_CHECK_INTERVAL_PROPERTY = "mule.launcher.changeCheckInterval";

    protected ScheduledExecutorService appDirMonitorTimer;

    protected transient final Log logger = LogFactory.getLog(getClass());
    protected MuleDeployer deployer;
    protected ApplicationFactory appFactory;
    // fair lock
    private ReentrantLock lock = new DebuggableReentrantLock(true);

    private ObservableList applications = new ObservableList();
    private Map zombieMap = new HashMap();
    private final File appsDir = MuleContainerBootstrapUtils.getMuleAppsDir();

    private List startupListeners = new ArrayList();

    private CompositeDeploymentListener deploymentListener = new CompositeDeploymentListener();

    public MuleDeploymentService(PluginClassLoaderManager pluginClassLoaderManager)
    {
        ApplicationClassLoaderFactory applicationClassLoaderFactory = new MuleApplicationClassLoaderFactory();
        applicationClassLoaderFactory = new CompositeApplicationClassLoaderFactory(applicationClassLoaderFactory, pluginClassLoaderManager);
        DefaultApplicationFactory appFactory = new DefaultApplicationFactory(applicationClassLoaderFactory);
        appFactory.setDeploymentListener(deploymentListener);
        this.appFactory = appFactory;

        DefaultMuleDeployer deployer = new DefaultMuleDeployer();
        deployer.setApplicationFactory(this.appFactory);
        this.deployer = deployer;
    }

    @Override
    public void start()
    {
        DeploymentStatusTracker deploymentStatusTracker = new DeploymentStatusTracker();
        addDeploymentListener(deploymentStatusTracker);

        StartupSummaryDeploymentListener summaryDeploymentListener = new StartupSummaryDeploymentListener(deploymentStatusTracker);
        addStartupListener(summaryDeploymentListener);

        deleteAllAnchors();

        // mule -app app1:app2:app3 will restrict deployment only to those specified apps
        final Map options = StartupContext.get().getStartupOptions();
        String appString = (String) options.get("app");

        if (appString == null)
        {
            String[] explodedApps = appsDir.list(DirectoryFileFilter.DIRECTORY);
            String[] packagedApps = appsDir.list(ZIP_APPS_FILTER);

            deployPackedApps(packagedApps);
            deployExplodedApps(explodedApps);
        }
        else
        {
            String[] apps = appString.split(":");
            apps = removeDuplicateAppNames(apps);

            for (String app : apps)
            {
                try
                {
                    File applicationFile = new File(appsDir, app + ZIP_FILE_SUFFIX);

                    if (applicationFile.exists() && applicationFile.isFile())
                    {
                        deployPackedApp(app + ZIP_FILE_SUFFIX);
                    }
                    else
                    {
                        deployExplodedApp(app);
                    }
                }
                catch (Exception e)
                {
                    // Ignore and continue
                }
            }
        }

        for (StartupListener listener : startupListeners)
        {
            try
            {
                listener.onAfterStartup();
            }
            catch (Throwable t)
            {
                logger.error(t);
            }
        }

        // only start the monitor thread if we launched in default mode without explicitly
        // stated applications to launch
        if (!(appString != null))
        {
            scheduleChangeMonitor(appsDir);
        }
        else
        {
            if (logger.isInfoEnabled())
            {
                logger.info(miniSplash("Mule is up and running in a fixed app set mode"));
            }
        }
    }

    private void deleteAllAnchors()
    {
        // Deletes any leftover anchor files from previous shutdowns
        String[] appAnchors = appsDir.list(new SuffixFileFilter(APP_ANCHOR_SUFFIX));
        for (String anchor : appAnchors)
        {
            // ignore result
            new File(appsDir, anchor).delete();
        }
    }

    private void deployApplication(Application application) throws DeploymentException
    {
        try
        {
            deploymentListener.onDeploymentStart(application.getAppName());
            guardedDeploy(application);
            deploymentListener.onDeploymentSuccess(application.getAppName());
            zombieMap.remove(application.getAppName());
        }
        catch (Throwable t)
        {
            // error text has been created by the deployer already
            String msg = miniSplash(String.format("Failed to deploy app '%s', see below", application.getAppName()));
            logger.error(msg, t);

            addZombieApp(application);

            deploymentListener.onDeploymentFailure(application.getAppName(), t);
            if (t instanceof DeploymentException)
            {
                throw (DeploymentException) t;
            }
            else
            {
                msg = "Failed to deploy application: " + application.getAppName();
                throw new DeploymentException(MessageFactory.createStaticMessage(msg), t);
            }
        }
    }

    private String[] removeDuplicateAppNames(String[] apps)
    {
        List appNames = new LinkedList();

        for (String appName : apps)
        {
            if (!appNames.contains(appName))
            {
                appNames.add(appName);
            }
        }

        return appNames.toArray(ArrayUtils.EMPTY_STRING_ARRAY);
    }

    protected void scheduleChangeMonitor(File appsDir)
    {
        final int reloadIntervalMs = getChangesCheckIntervalMs();
        appDirMonitorTimer = Executors.newSingleThreadScheduledExecutor(new AppDeployerMonitorThreadFactory());

        appDirMonitorTimer.scheduleWithFixedDelay(new AppDirWatcher(appsDir),
                                                  0,
                                                  reloadIntervalMs,
                                                  TimeUnit.MILLISECONDS);

        if (logger.isInfoEnabled())
        {
            logger.info(miniSplash(String.format("Mule is up and kicking (every %dms)", reloadIntervalMs)));
        }
    }

    public static int getChangesCheckIntervalMs()
    {
        try
        {
            String value = System.getProperty(CHANGE_CHECK_INTERVAL_PROPERTY);
            return Integer.parseInt(value);
        }
        catch (NumberFormatException e)
        {
            return DEFAULT_CHANGES_CHECK_INTERVAL_MS;
        }
    }

    @Override
    public void stop()
    {
        stopAppDirMonitorTimer();

        lock.lock();
        try
        {
            // tear down apps in reverse order
            Collections.reverse(applications);

            for (Application application : applications)
            {
                try
                {
                    application.stop();
                    application.dispose();
                }
                catch (Throwable t)
                {
                    logger.error(t);
                }
            }
        }
        finally
        {
            lock.unlock();
        }
    }

    private void stopAppDirMonitorTimer()
    {
        if (appDirMonitorTimer != null)
        {
            appDirMonitorTimer.shutdown();
            try
            {
                appDirMonitorTimer.awaitTermination(getChangesCheckIntervalMs(), TimeUnit.MILLISECONDS);
            }
            catch (InterruptedException e)
            {
                throw new RuntimeException(e);
            }
        }
    }

    @Override
    public Application findApplication(String appName)
    {
        return (Application) CollectionUtils.find(applications, new BeanPropertyValueEqualsPredicate("appName", appName));
    }

    @Override
    public List getApplications()
    {
        return Collections.unmodifiableList(applications);
    }

    /**
     * @return URL/lastModified of apps which previously failed to deploy
     */
    public Map getZombieMap()
    {
        Map result = new HashMap();

        for (String app : zombieMap.keySet())
        {
            ZombieFile file = zombieMap.get(app);
            result.put(file.url, file.lastUpdated);
        }

        return result;
    }

    protected MuleDeployer getDeployer()
    {
        return deployer;
    }

    public void setAppFactory(ApplicationFactory appFactory)
    {
        this.appFactory = appFactory;
    }

    public void setDeployer(MuleDeployer deployer)
    {
        this.deployer = deployer;
    }

    public ApplicationFactory getAppFactory()
    {
        return appFactory;
    }

    @Override
    public ReentrantLock getLock() {
        return lock;
    }

    protected void onApplicationInstalled(Application a)
    {
        trackApplication(a);
    }

    private void trackApplication(Application application)
    {
        Application previousApplication = findApplication(application.getAppName());
        applications.remove(previousApplication);

        applications.add(application);
    }

    protected void undeploy(Application app)
    {
        if (logger.isInfoEnabled())
        {
            logger.info("================== Request to Undeploy Application: " + app.getAppName());
        }

        try
        {
            deploymentListener.onUndeploymentStart(app.getAppName());

            applications.remove(app);
            guardedUndeploy(app);

            deploymentListener.onUndeploymentSuccess(app.getAppName());
        }
        catch (RuntimeException e)
        {
            deploymentListener.onUndeploymentFailure(app.getAppName(), e);
            throw e;
        }
    }

    @Override
    public void undeploy(String appName)
    {
        Application app = (Application) CollectionUtils.find(applications, new BeanPropertyValueEqualsPredicate("appName", appName));
        undeploy(app);
    }

    @Override
    public void deploy(URL appArchiveUrl) throws IOException
    {
        Application application;

        try
        {
            try
            {
                application = guardedInstallFrom(appArchiveUrl);
                trackApplication(application);
            }
            catch (Throwable t)
            {
                File appArchive = new File(appArchiveUrl.toURI());
                String appName = StringUtils.removeEnd(appArchive.getName(), ZIP_FILE_SUFFIX);

                //// error text has been created by the deployer already
                final String msg = miniSplash(String.format("Failed to deploy app '%s', see below", appName));
                logger.error(msg, t);

                addZombieFile(appName, appArchive);

                deploymentListener.onDeploymentFailure(appName, t);

                throw t;
            }

            deployApplication(application);
        }
        catch (Throwable t)
        {
            if (t instanceof DeploymentException)
            {
                // re-throw
                throw ((DeploymentException) t);
            }

            final String msg = "Failed to deploy from URL: " + appArchiveUrl;
            throw new DeploymentException(MessageFactory.createStaticMessage(msg), t);
        }
    }

    private void guardedDeploy(Application application)
    {
        try
        {
            if (!lock.tryLock(0, TimeUnit.SECONDS))
            {
                return;
            }

            deployer.deploy(application);
        }
        catch (InterruptedException e)
        {
            Thread.currentThread().interrupt();
        }
        finally
        {
            if (lock.isHeldByCurrentThread())
            {
                lock.unlock();
            }
        }
    }

    private void guardedUndeploy(Application app)
    {
        try
        {
            if (!lock.tryLock(0, TimeUnit.SECONDS))
            {
                return;
            }

            deployer.undeploy(app);
        }
        catch (InterruptedException e)
        {
            Thread.currentThread().interrupt();
        }
        finally
        {
            if (lock.isHeldByCurrentThread())
            {
                lock.unlock();
            }
        }
    }

    private Application guardedInstallFrom(URL appArchiveUrl) throws IOException
    {
        try
        {
            if (!lock.tryLock(0, TimeUnit.SECONDS))
            {
                throw new IOException(ANOTHER_DEPLOYMENT_OPERATION_IS_IN_PROGRESS);
            }

            return deployer.installFrom(appArchiveUrl);
        }
        catch (InterruptedException e)
        {
            Thread.currentThread().interrupt();
            throw new IOException(INSTALL_OPERATION_HAS_BEEN_INTERRUPTED);
        }
        finally
        {
            if (lock.isHeldByCurrentThread())
            {
                lock.unlock();
            }
        }
    }

    protected void addZombieApp(Application application)
    {
        final File appDir = new File(MuleContainerBootstrapUtils.getMuleAppsDir(), application.getAppName()) ;

        String resource = application.getDescriptor().getConfigResources()[0];
        File resourceFile = new File(appDir, resource);
        ZombieFile zombieFile = new ZombieFile();

        if (resourceFile.exists())
        {
            try
            {
                zombieFile.url = resourceFile.toURI().toURL();
                zombieFile.lastUpdated = resourceFile.lastModified();

                zombieMap.put(application.getAppName(), zombieFile);
            }
            catch (MalformedURLException e)
            {
                // Ignore resource
            }
        }
    }

    protected void addZombieFile(String appName, File marker)
    {
        // no sync required as deploy operations are single-threaded
        if (marker == null)
        {
            return;
        }

        if (!marker.exists())
        {
            return;
        }

        try
        {
            long lastModified = marker.lastModified();

            ZombieFile zombieFile = new ZombieFile();
            zombieFile.url = marker.toURI().toURL();
            zombieFile.lastUpdated = lastModified;

            zombieMap.put(appName, zombieFile);
        }
        catch (MalformedURLException e)
        {
            logger.debug(String.format("Failed to mark an exploded app [%s] as a zombie", marker.getName()), e);
        }
    }

    @Override
    public void addStartupListener(StartupListener listener)
    {
        this.startupListeners.add(listener);
    }

    @Override
    public void removeStartupListener(StartupListener listener)
    {
        this.startupListeners.remove(listener);
    }

    @Override
    public void addDeploymentListener(DeploymentListener listener)
    {
        deploymentListener.addDeploymentListener(listener);
    }

    @Override
    public void removeDeploymentListener(DeploymentListener listener)
    {
        deploymentListener.removeDeploymentListener(listener);
    }

    private void deployPackedApps(String[] zips)
    {
        for (String zip : zips)
        {
            try
            {
                deployPackedApp(zip);
            }
            catch (Exception e)
            {
                // Ignore and continue
            }
        }
    }

    private void deployPackedApp(String zip) throws Exception
    {
        URL url;
        File appZip;

        final String appName = StringUtils.removeEnd(zip, ZIP_FILE_SUFFIX);

        appZip = new File(appsDir, zip);
        url = appZip.toURI().toURL();

        ZombieFile zombieFile = zombieMap.get(appName);
        if (zombieFile != null)
        {
            if (isZombieFile(url, zombieFile) && !updatedZombieApp(zombieFile))
            {
                // Skips the file because it was already deployed with failure
                return;
            }
        }

        // check if this app is running first, undeploy it then
        Application app = (Application) CollectionUtils.find(applications, new BeanPropertyValueEqualsPredicate("appName", appName));
        if (app != null)
        {
            undeploy(appName);
        }

        deploy(url);
    }

    private void deployExplodedApps(String[] apps)
    {
        @SuppressWarnings("rawtypes")
        Collection deployedAppNames = CollectionUtils.collect(applications, new BeanToPropertyValueTransformer("appName"));

        for (String addedApp : apps)
        {
            ZombieFile zombieFile = zombieMap.get(addedApp);

            if ((zombieFile != null) && (!updatedZombieApp(zombieFile)))
            {
                continue;
            }

            if (deployedAppNames.contains(addedApp) && (!zombieMap.containsKey(addedApp)))
            {
                continue;
            }

            try
            {
                deployExplodedApp(addedApp);
            }
            catch (DeploymentException e)
            {
                // Ignore and continue
            }
        }
    }

    private void deployExplodedApp(String addedApp) throws DeploymentException
    {
        if (logger.isInfoEnabled())
        {
            logger.info("================== New Exploded Application: " + addedApp);
        }

        Application application;
        try
        {
            application = appFactory.createApp(addedApp);

            // add to the list of known apps first to avoid deployment loop on failure
            onApplicationInstalled(application);
        }
        catch (Throwable t)
        {
            final File appsDir1 = MuleContainerBootstrapUtils.getMuleAppsDir();
            File appDir1 = new File(appsDir1, addedApp);

            addZombieFile(addedApp, appDir1);

            String msg = miniSplash(String.format("Failed to deploy exploded application: '%s', see below", addedApp));
            logger.error(msg, t);

            deploymentListener.onDeploymentFailure(addedApp, t);

            if (t instanceof DeploymentException)
            {
                throw (DeploymentException) t;
            }
            else
            {
                msg = "Failed to deploy application: " + addedApp;
                throw new DeploymentException(MessageFactory.createStaticMessage(msg), t);
            }
        }

        deployApplication(application);
    }

    private boolean isZombieFile(URL url, ZombieFile zombieFile)
    {
        return zombieFile.url.equals(url);
    }

    private boolean updatedZombieApp(ZombieFile zombieFile)
    {
        long currentTimeStamp = FileUtils.getFileTimeStamp(zombieFile.url);

        return zombieFile.lastUpdated != currentTimeStamp;
    }

    /**
     * Returns the list of anchor file names for the deployed apps
     *
     * @return a non null list of file names
     */
    private String[] findExpectedAnchorFiles()
    {
        String[] appAnchors = new String[applications.size()];
        int i = 0;

        for (Application application : applications)
        {
            appAnchors[i++] = application.getAppName() + APP_ANCHOR_SUFFIX;
        }

        return appAnchors;
    }

    /**
     * Not thread safe. Correctness is guaranteed by a single-threaded executor.
     */
    protected class AppDirWatcher implements Runnable
    {
        protected File appsDir;

        protected volatile boolean dirty;

        public AppDirWatcher(final File appsDir)
        {
            this.appsDir = appsDir;
            applications.addPropertyChangeListener(new PropertyChangeListener()
            {
                public void propertyChange(PropertyChangeEvent e)
                {
                    if (e instanceof ElementAddedEvent || e instanceof ElementRemovedEvent)
                    {
                        if (logger.isDebugEnabled())
                        {
                            logger.debug("Deployed applications set has been modified, flushing state.");
                        }
                        dirty = true;
                    }
                }
            });
        }

        // Cycle is:
        //   undeploy removed apps
        //   deploy archives
        //   deploy exploded
        public void run()
        {
            try
            {
                if (logger.isDebugEnabled())
                {
                    logger.debug("Checking for changes...");
                }
                // use non-barging lock to preserve fairness, according to javadocs
                // if there's a lock present - wait for next poll to do anything
                if (!lock.tryLock(0, TimeUnit.SECONDS))
                {
                    if (logger.isDebugEnabled())
                    {
                        logger.debug("Another deployment operation in progress, will skip this cycle. Owner thread: " +
                                     ((DebuggableReentrantLock) lock).getOwner());
                    }
                    return;
                }

                undeployRemovedApps();

                // list new apps
                String[] apps = appsDir.list(DirectoryFileFilter.DIRECTORY);

                final String[] zips = appsDir.list(ZIP_APPS_FILTER);

                deployPackedApps(zips);

                // re-scan exploded apps and update our state, as deploying Mule app archives might have added some
                if (zips.length > 0 || dirty)
                {
                    apps = appsDir.list(DirectoryFileFilter.DIRECTORY);
                }

                deployExplodedApps(apps);
            }
            catch (InterruptedException e)
            {
                // preserve the flag for the thread
                Thread.currentThread().interrupt();
            }
            finally
            {
                if (lock.isHeldByCurrentThread())
                {
                    lock.unlock();
                }
                dirty = false;
            }
        }

        private void undeployRemovedApps()
        {
            // we care only about removed anchors
            String[] currentAnchors = appsDir.list(new SuffixFileFilter(APP_ANCHOR_SUFFIX));
            if (logger.isDebugEnabled())
            {
                StringBuilder sb = new StringBuilder();
                sb.append(String.format("Current anchors:%n"));
                for (String currentAnchor : currentAnchors)
                {
                    sb.append(String.format("  %s%n", currentAnchor));
                }
                logger.debug(sb.toString());
            }

            String[] appAnchors = findExpectedAnchorFiles();
            @SuppressWarnings("unchecked")
            final Collection deletedAnchors = CollectionUtils.subtract(Arrays.asList(appAnchors), Arrays.asList(currentAnchors));
            if (logger.isDebugEnabled())
            {
                StringBuilder sb = new StringBuilder();
                sb.append(String.format("Deleted anchors:%n"));
                for (String deletedAnchor : deletedAnchors)
                {
                    sb.append(String.format("  %s%n", deletedAnchor));
                }
                logger.debug(sb.toString());
            }

            for (String deletedAnchor : deletedAnchors)
            {
                String appName = StringUtils.removeEnd(deletedAnchor, APP_ANCHOR_SUFFIX);
                try
                {
                    if (zombieMap.containsKey(appName))
                    {
                        continue;
                    }

                    if (findApplication(appName) != null)
                    {
                        undeploy(appName);
                    }
                    else if (logger.isDebugEnabled())
                    {
                        logger.debug(String.format("Application [%s] has already been undeployed via API", appName));
                    }
                }
                catch (Throwable t)
                {
                    logger.error("Failed to undeploy application: " + appName, t);
                }
            }
        }
    }

    private static class ZombieFile
    {
        URL url;
        Long lastUpdated;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy