Coverage Report - org.mule.module.launcher.DeploymentService
 
Classes in this File Line Coverage Branch Coverage Complexity
DeploymentService
0%
0/101
0%
0/30
0
DeploymentService$AppDirWatcher
0%
0/76
0%
0/36
0
DeploymentService$AppDirWatcher$1
0%
0/6
0%
0/6
0
 
 1  
 /*
 2  
  * $Id: DeploymentService.java 20719 2010-12-14 19:23:28Z aperepel $
 3  
  * --------------------------------------------------------------------------------------
 4  
  * Copyright (c) MuleSoft, Inc.  All rights reserved.  http://www.mulesoft.com
 5  
  *
 6  
  * The software in this package is published under the terms of the CPAL v1.0
 7  
  * license, a copy of which has been included with this distribution in the
 8  
  * LICENSE.txt file.
 9  
  */
 10  
 
 11  
 package org.mule.module.launcher;
 12  
 
 13  
 import org.mule.config.StartupContext;
 14  
 import org.mule.config.i18n.MessageFactory;
 15  
 import org.mule.module.launcher.application.Application;
 16  
 import org.mule.module.launcher.application.ApplicationFactory;
 17  
 import org.mule.module.launcher.util.DebuggableReentrantLock;
 18  
 import org.mule.module.launcher.util.ElementAddedEvent;
 19  
 import org.mule.module.launcher.util.ElementRemovedEvent;
 20  
 import org.mule.module.launcher.util.ObservableList;
 21  
 import org.mule.module.reboot.MuleContainerBootstrapUtils;
 22  
 import org.mule.util.CollectionUtils;
 23  
 import org.mule.util.StringUtils;
 24  
 
 25  
 import java.beans.PropertyChangeEvent;
 26  
 import java.beans.PropertyChangeListener;
 27  
 import java.io.File;
 28  
 import java.io.IOException;
 29  
 import java.net.MalformedURLException;
 30  
 import java.net.URL;
 31  
 import java.util.Arrays;
 32  
 import java.util.Collection;
 33  
 import java.util.Collections;
 34  
 import java.util.HashMap;
 35  
 import java.util.List;
 36  
 import java.util.Map;
 37  
 import java.util.concurrent.Executors;
 38  
 import java.util.concurrent.ScheduledExecutorService;
 39  
 import java.util.concurrent.TimeUnit;
 40  
 import java.util.concurrent.locks.ReentrantLock;
 41  
 
 42  
 import org.apache.commons.beanutils.BeanPropertyValueEqualsPredicate;
 43  
 import org.apache.commons.beanutils.BeanToPropertyValueTransformer;
 44  
 import org.apache.commons.io.filefilter.DirectoryFileFilter;
 45  
 import org.apache.commons.io.filefilter.SuffixFileFilter;
 46  
 import org.apache.commons.logging.Log;
 47  
 import org.apache.commons.logging.LogFactory;
 48  
 
 49  0
 public class DeploymentService
 50  
 {
 51  
     public static final String APP_ANCHOR_SUFFIX = "-anchor.txt";
 52  
     protected static final int DEFAULT_CHANGES_CHECK_INTERVAL_MS = 5000;
 53  
 
 54  
     protected ScheduledExecutorService appDirMonitorTimer;
 55  
 
 56  0
     protected transient final Log logger = LogFactory.getLog(getClass());
 57  
     protected MuleDeployer deployer;
 58  
     protected ApplicationFactory appFactory;
 59  
     // fair lock
 60  0
     private ReentrantLock lock = new DebuggableReentrantLock(true);
 61  
 
 62  0
     private ObservableList<Application> applications = new ObservableList<Application>();
 63  0
     private Map<URL, Long> zombieMap = new HashMap<URL, Long>();
 64  0
     private ObservableList<URL> zombieLand = new ObservableList<URL>();
 65  
 
 66  
     public DeploymentService()
 67  0
     {
 68  0
         deployer = new DefaultMuleDeployer(this);
 69  0
         appFactory = new ApplicationFactory(this);
 70  0
     }
 71  
 
 72  
     public void start()
 73  
     {
 74  
         // install phase
 75  0
         final Map<String, Object> options = StartupContext.get().getStartupOptions();
 76  0
         String appString = (String) options.get("app");
 77  
 
 78  0
         final File appsDir = MuleContainerBootstrapUtils.getMuleAppsDir();
 79  
 
 80  
         // delete any leftover anchor files from previous unclean shutdowns
 81  0
         String[] appAnchors = appsDir.list(new SuffixFileFilter(APP_ANCHOR_SUFFIX));
 82  0
         for (String anchor : appAnchors)
 83  
         {
 84  
             // ignore result
 85  0
             new File(appsDir, anchor).delete();
 86  
         }
 87  
 
 88  
         String[] apps;
 89  
 
 90  
         // mule -app app1:app2:app3 will restrict deployment only to those specified apps
 91  0
         final boolean explicitAppSet = appString != null;
 92  
 
 93  0
         if (!explicitAppSet)
 94  
         {
 95  
             // explode any app zips first
 96  0
             final String[] zips = appsDir.list(new SuffixFileFilter(".zip"));
 97  0
             Arrays.sort(zips);
 98  0
             for (String zip : zips)
 99  
             {
 100  
                 try
 101  
                 {
 102  
                     // we don't care about the returned app object on startup
 103  0
                     deployer.installFromAppDir(zip);
 104  
                 }
 105  0
                 catch (Throwable t)
 106  
                 {
 107  0
                     logger.error(String.format("Failed to install app from archive '%s'", zip), t);
 108  0
                     File appFile = new File(appsDir, zip);
 109  
                     try
 110  
                     {
 111  0
                         addZombie(appFile.toURL());
 112  
                     }
 113  0
                     catch (MalformedURLException muex)
 114  
                     {
 115  0
                         if (logger.isDebugEnabled())
 116  
                         {
 117  0
                             logger.debug(muex);
 118  
                         }
 119  0
                     }
 120  0
                 }
 121  
             }
 122  
 
 123  
             // TODO this is a place to put a FQN of the custom sorter (use AND filter)
 124  
             // Add string shortcuts for bundled ones
 125  0
             apps = appsDir.list(DirectoryFileFilter.DIRECTORY);
 126  0
             Arrays.sort(apps);
 127  0
         }
 128  
         else
 129  
         {
 130  0
             apps = appString.split(":");
 131  
         }
 132  
 
 133  0
         for (String app : apps)
 134  
         {
 135  
             final Application a;
 136  
             try
 137  
             {
 138  0
                 a = appFactory.createApp(app);
 139  0
                 applications.add(a);
 140  
             }
 141  0
             catch (IOException e)
 142  
             {
 143  
                 // TODO logging
 144  0
                 e.printStackTrace();
 145  0
             }
 146  
         }
 147  
 
 148  
 
 149  0
         for (Application application : applications)
 150  
         {
 151  
             try
 152  
             {
 153  0
                 deployer.deploy(application);
 154  
             }
 155  0
             catch (Throwable t)
 156  
             {
 157  
                 // TODO logging
 158  0
                 t.printStackTrace();
 159  0
             }
 160  
         }
 161  
 
 162  
         // only start the monitor thread if we launched in default mode without explicitly
 163  
         // stated applications to launch
 164  0
         if (!explicitAppSet)
 165  
         {
 166  0
             scheduleChangeMonitor(appsDir);
 167  
         }
 168  0
     }
 169  
 
 170  
     protected void scheduleChangeMonitor(File appsDir)
 171  
     {
 172  0
         final int reloadIntervalMs = DEFAULT_CHANGES_CHECK_INTERVAL_MS;
 173  0
         appDirMonitorTimer = Executors.newSingleThreadScheduledExecutor(new AppDeployerMonitorThreadFactory());
 174  
 
 175  0
         appDirMonitorTimer.scheduleWithFixedDelay(new AppDirWatcher(appsDir),
 176  
                                                   0,
 177  
                                                   reloadIntervalMs,
 178  
                                                   TimeUnit.MILLISECONDS);
 179  
 
 180  0
         if (logger.isInfoEnabled())
 181  
         {
 182  0
             logger.info("Application directory check interval: " + reloadIntervalMs);
 183  
         }
 184  0
     }
 185  
 
 186  
     public void stop()
 187  
     {
 188  0
         if (appDirMonitorTimer != null)
 189  
         {
 190  0
             appDirMonitorTimer.shutdownNow();
 191  
         }
 192  
 
 193  
         // tear down apps in reverse order
 194  0
         Collections.reverse(applications);
 195  0
         for (Application application : applications)
 196  
         {
 197  
             try
 198  
             {
 199  0
                 application.stop();
 200  0
                 application.dispose();
 201  
             }
 202  0
             catch (Throwable t)
 203  
             {
 204  
                 // TODO logging
 205  0
                 t.printStackTrace();
 206  0
             }
 207  
         }
 208  
 
 209  0
     }
 210  
 
 211  
     /**
 212  
      * Find an active application.
 213  
      * @return null if not found
 214  
      */
 215  
     public Application findApplication(String appName)
 216  
     {
 217  0
         return (Application) CollectionUtils.find(applications, new BeanPropertyValueEqualsPredicate("appName", appName));
 218  
     }
 219  
 
 220  
     /**
 221  
      * @return immutable applications list
 222  
      */
 223  
     public List<Application> getApplications()
 224  
     {
 225  0
         return Collections.unmodifiableList(applications);
 226  
     }
 227  
 
 228  
     /**
 229  
      * @return URL/lastModified of apps which previously failed to deploy
 230  
      */
 231  
     public Map<URL, Long> getZombieMap()
 232  
     {
 233  0
         return zombieMap;
 234  
     }
 235  
 
 236  
 
 237  
 
 238  
     protected MuleDeployer getDeployer()
 239  
     {
 240  0
         return deployer;
 241  
     }
 242  
 
 243  
     public void setDeployer(MuleDeployer deployer)
 244  
     {
 245  0
         this.deployer = deployer;
 246  0
     }
 247  
 
 248  
     public ApplicationFactory getAppFactory()
 249  
     {
 250  0
         return appFactory;
 251  
     }
 252  
 
 253  
     public ReentrantLock getLock() {
 254  0
         return lock;
 255  
     }
 256  
 
 257  
     public void onApplicationInstalled(Application a)
 258  
     {
 259  0
         applications.add(a);
 260  0
     }
 261  
 
 262  
     protected void undeploy(Application app)
 263  
     {
 264  0
         if (logger.isInfoEnabled())
 265  
         {
 266  0
             logger.info("================== Request to Undeploy Application: " + app.getAppName());
 267  
         }
 268  
 
 269  0
         deployer.undeploy(app);
 270  0
     }
 271  
 
 272  
     public void undeploy(String appName)
 273  
     {
 274  0
         Application app = (Application) CollectionUtils.find(applications, new BeanPropertyValueEqualsPredicate("appName", appName));
 275  0
         applications.remove(app);
 276  0
         deployer.undeploy(app);
 277  0
     }
 278  
 
 279  
     public void deploy(URL appArchiveUrl) throws IOException
 280  
     {
 281  
         final Application application;
 282  
         try
 283  
         {
 284  0
             application = deployer.installFrom(appArchiveUrl);
 285  0
             applications.add(application);
 286  0
             deployer.deploy(application);
 287  
         }
 288  0
         catch (Throwable t)
 289  
         {
 290  0
             addZombie(appArchiveUrl);
 291  0
             if (t instanceof DeploymentException)
 292  
             {
 293  
                 // re-throw
 294  0
                 throw ((DeploymentException) t);
 295  
             }
 296  
 
 297  0
             final String msg = "Failed to deploy from URL: " + appArchiveUrl;
 298  0
             throw new DeploymentException(MessageFactory.createStaticMessage(msg), t);
 299  0
         }
 300  0
     }
 301  
 
 302  
     protected void addZombie(URL appArchiveUrl)
 303  
     {
 304  
         // no sync required as deploy operations are single-threaded
 305  0
         if (appArchiveUrl == null)
 306  
         {
 307  0
             return;
 308  
         }
 309  
 
 310  0
         long lastModified = -1;
 311  
         // get timestamp only from file:// urls
 312  0
         if ("file".equals(appArchiveUrl.getProtocol()))
 313  
         {
 314  0
             lastModified = new File(appArchiveUrl.getFile()).lastModified();
 315  
         }
 316  
 
 317  0
         zombieMap.put(appArchiveUrl, lastModified);
 318  0
     }
 319  
 
 320  
     /**
 321  
      * Not thread safe. Correctness is guaranteed by a single-threaded executor.
 322  
      */
 323  
     protected class AppDirWatcher implements Runnable
 324  
     {
 325  
         protected File appsDir;
 326  
 
 327  
         // written on app start, will be used to cleanly undeploy the app without file locking issues
 328  0
         protected String[] appAnchors = new String[0];
 329  
         protected volatile boolean dirty;
 330  
 
 331  
         public AppDirWatcher(final File appsDir)
 332  0
         {
 333  0
             this.appsDir = appsDir;
 334  0
             applications.addPropertyChangeListener(new PropertyChangeListener()
 335  0
             {
 336  
                 public void propertyChange(PropertyChangeEvent e)
 337  
                 {
 338  0
                     if (e instanceof ElementAddedEvent || e instanceof ElementRemovedEvent)
 339  
                     {
 340  0
                         if (logger.isDebugEnabled())
 341  
                         {
 342  0
                             logger.debug("Deployed applications set has been modified, flushing state.");
 343  
                         }
 344  0
                         dirty = true;
 345  
                     }
 346  0
                 }
 347  
             });
 348  0
         }
 349  
 
 350  
         // Cycle is:
 351  
         //   undeploy removed apps
 352  
         //   deploy archives
 353  
         //   deploy exploded
 354  
         public void run()
 355  
         {
 356  
             try
 357  
             {
 358  0
                 if (logger.isDebugEnabled())
 359  
                 {
 360  0
                     logger.debug("Checking for changes...");
 361  
                 }
 362  
                 // use non-barging lock to preserve fairness, according to javadocs
 363  
                 // if there's a lock present - wait for next poll to do anything
 364  0
                 if (!lock.tryLock(0, TimeUnit.SECONDS))
 365  
                 {
 366  0
                     if (logger.isDebugEnabled())
 367  
                     {
 368  0
                         logger.debug("Another deployment operation in progress, will skip this cycle. Owner thread: " +
 369  
                                      ((DebuggableReentrantLock) lock).getOwner());
 370  
                     }
 371  
                     return;
 372  
                 }
 373  
 
 374  
 
 375  
                 // list new apps
 376  0
                 final String[] zips = appsDir.list(new SuffixFileFilter(".zip"));
 377  0
                 String[] apps = appsDir.list(DirectoryFileFilter.DIRECTORY);
 378  
 
 379  
 
 380  
                 // we care only about removed anchors
 381  0
                 String[] currentAnchors = appsDir.list(new SuffixFileFilter(APP_ANCHOR_SUFFIX));
 382  0
                 if (logger.isDebugEnabled())
 383  
                 {
 384  0
                     StringBuilder sb = new StringBuilder();
 385  0
                     sb.append(String.format("Current anchors:%n"));
 386  0
                     for (String currentAnchor : currentAnchors)
 387  
                     {
 388  0
                         sb.append(String.format("  %s%n", currentAnchor));
 389  
                     }
 390  0
                     logger.debug(sb.toString());
 391  
                 }
 392  
                 @SuppressWarnings("unchecked")
 393  0
                 final Collection<String> deletedAnchors = CollectionUtils.subtract(Arrays.asList(appAnchors), Arrays.asList(currentAnchors));
 394  0
                 if (logger.isDebugEnabled())
 395  
                 {
 396  0
                     StringBuilder sb = new StringBuilder();
 397  0
                     sb.append(String.format("Deleted anchors:%n"));
 398  0
                     for (String deletedAnchor : deletedAnchors)
 399  
                     {
 400  0
                         sb.append(String.format("  %s%n", deletedAnchor));
 401  
                     }
 402  0
                     logger.debug(sb.toString());
 403  
                 }
 404  
 
 405  0
                 for (String deletedAnchor : deletedAnchors)
 406  
                 {
 407  0
                     String appName = StringUtils.removeEnd(deletedAnchor, APP_ANCHOR_SUFFIX);
 408  
                     try
 409  
                     {
 410  0
                         if (findApplication(appName) != null)
 411  
                         {
 412  0
                             undeploy(appName);
 413  
                         }
 414  0
                         else if (logger.isDebugEnabled())
 415  
                         {
 416  0
                             logger.debug(String.format("Application [%s] has already been undeployed via API", appName));
 417  
                         }
 418  
                     }
 419  0
                     catch (Throwable t)
 420  
                     {
 421  0
                         logger.error("Failed to undeploy application: " + appName, t);
 422  0
                     }
 423  0
                 }
 424  0
                 appAnchors = currentAnchors;
 425  
 
 426  
 
 427  
                 // new packed Mule apps
 428  0
                 for (String zip : zips)
 429  
                 {
 430  0
                     URL url = null;
 431  
                     try
 432  
                     {
 433  
                         // check if this app is running first, undeploy it then
 434  0
                         final String appName = StringUtils.removeEnd(zip, ".zip");
 435  0
                         Application app = (Application) CollectionUtils.find(applications, new BeanPropertyValueEqualsPredicate("appName", appName));
 436  0
                         if (app != null)
 437  
                         {
 438  0
                             undeploy(appName);
 439  
                         }
 440  0
                         url = new File(appsDir, zip).toURI().toURL();
 441  0
                         deploy(url);
 442  
                     }
 443  0
                     catch (Throwable t)
 444  
                     {
 445  0
                         logger.error("Failed to deploy application archive: " + zip, t);
 446  0
                         addZombie(url);
 447  0
                     }
 448  
                 }
 449  
 
 450  
                 // re-scan exploded apps and update our state, as deploying Mule app archives might have added some
 451  0
                 if (zips.length > 0 || dirty)
 452  
                 {
 453  0
                     apps = appsDir.list(DirectoryFileFilter.DIRECTORY);
 454  
                 }
 455  
 
 456  0
                 Collection deployedAppNames = CollectionUtils.collect(applications, new BeanToPropertyValueTransformer("appName"));
 457  
 
 458  
                 // new exploded Mule apps
 459  
                 @SuppressWarnings("unchecked")
 460  0
                 final Collection<String> addedApps = CollectionUtils.subtract(Arrays.asList(apps), deployedAppNames);
 461  0
                 for (String addedApp : addedApps)
 462  
                 {
 463  
                     try
 464  
                     {
 465  0
                         onNewExplodedApplication(addedApp);
 466  
                     }
 467  0
                     catch (Throwable t)
 468  
                     {
 469  0
                         logger.error("Failed to deploy exploded application: " + addedApp, t);
 470  
                         try
 471  
                         {
 472  0
                             addZombie(new File(appsDir, addedApp).toURI().toURL());
 473  
                         }
 474  0
                         catch (MalformedURLException e)
 475  
                         {
 476  0
                             if (logger.isDebugEnabled())
 477  
                             {
 478  0
                                 logger.debug(e);
 479  
                             }
 480  0
                         }
 481  0
                     }
 482  
                 }
 483  
 
 484  
             }
 485  0
             catch (InterruptedException e)
 486  
             {
 487  
                 // preserve the flag for the thread
 488  0
                 Thread.currentThread().interrupt();
 489  
             }
 490  
             finally
 491  
             {
 492  0
                 if (lock.isHeldByCurrentThread())
 493  
                 {
 494  0
                     lock.unlock();
 495  
                 }
 496  0
                 dirty = false;
 497  0
             }
 498  0
         }
 499  
 
 500  
         /**
 501  
          * @param appName application name as it appears in $MULE_HOME/apps
 502  
          */
 503  
         protected void onNewExplodedApplication(String appName) throws Exception
 504  
         {
 505  0
             if (logger.isInfoEnabled())
 506  
             {
 507  0
                 logger.info("================== New Exploded Application: " + appName);
 508  
             }
 509  
 
 510  0
             Application a = appFactory.createApp(appName);
 511  
             // add to the list of known apps first to avoid deployment loop on failure
 512  0
             onApplicationInstalled(a);
 513  0
             deployer.deploy(a);
 514  0
         }
 515  
 
 516  
     }
 517  
 
 518  
 }