View Javadoc
1   /*
2    * Copyright (c) MuleSoft, Inc.  All rights reserved.  http://www.mulesoft.com
3    * The software in this package is published under the terms of the CPAL v1.0
4    * license, a copy of which has been included with this distribution in the
5    * LICENSE.txt file.
6    */
7   package org.mule.module.launcher;
8   
9   import static org.mule.util.SplashScreen.miniSplash;
10  import org.mule.config.StartupContext;
11  import org.mule.config.i18n.MessageFactory;
12  import org.mule.module.launcher.application.Application;
13  import org.mule.module.launcher.application.ApplicationFactory;
14  import org.mule.module.launcher.util.DebuggableReentrantLock;
15  import org.mule.module.launcher.util.ElementAddedEvent;
16  import org.mule.module.launcher.util.ElementRemovedEvent;
17  import org.mule.module.launcher.util.ObservableList;
18  import org.mule.module.reboot.MuleContainerBootstrapUtils;
19  import org.mule.util.ArrayUtils;
20  import org.mule.util.CollectionUtils;
21  import org.mule.util.FileUtils;
22  import org.mule.util.StringUtils;
23  
24  import java.beans.PropertyChangeEvent;
25  import java.beans.PropertyChangeListener;
26  import java.io.File;
27  import java.io.IOException;
28  import java.net.MalformedURLException;
29  import java.net.URL;
30  import java.util.ArrayList;
31  import java.util.Arrays;
32  import java.util.Collection;
33  import java.util.Collections;
34  import java.util.HashMap;
35  import java.util.LinkedList;
36  import java.util.List;
37  import java.util.Map;
38  import java.util.concurrent.CopyOnWriteArrayList;
39  import java.util.concurrent.Executors;
40  import java.util.concurrent.ScheduledExecutorService;
41  import java.util.concurrent.TimeUnit;
42  import java.util.concurrent.locks.ReentrantLock;
43  
44  import org.apache.commons.beanutils.BeanPropertyValueEqualsPredicate;
45  import org.apache.commons.beanutils.BeanToPropertyValueTransformer;
46  import org.apache.commons.io.filefilter.AndFileFilter;
47  import org.apache.commons.io.filefilter.DirectoryFileFilter;
48  import org.apache.commons.io.filefilter.FileFileFilter;
49  import org.apache.commons.io.filefilter.IOFileFilter;
50  import org.apache.commons.io.filefilter.SuffixFileFilter;
51  import org.apache.commons.logging.Log;
52  import org.apache.commons.logging.LogFactory;
53  
54  public class DeploymentService
55  {
56      public static final String APP_ANCHOR_SUFFIX = "-anchor.txt";
57      public static final String ZIP_FILE_SUFFIX = ".zip";
58      public static final IOFileFilter ZIP_APPS_FILTER = new AndFileFilter(new SuffixFileFilter(ZIP_FILE_SUFFIX), FileFileFilter.FILE);
59  
60      protected static final int DEFAULT_CHANGES_CHECK_INTERVAL_MS = 5000;
61  
62      protected ScheduledExecutorService appDirMonitorTimer;
63  
64      protected transient final Log logger = LogFactory.getLog(getClass());
65      protected MuleDeployer deployer;
66      protected ApplicationFactory appFactory;
67      // fair lock
68      private ReentrantLock lock = new DebuggableReentrantLock(true);
69  
70      private ObservableList<Application> applications = new ObservableList<Application>();
71      private Map<URL, Long> zombieMap = new HashMap<URL, Long>();
72  
73      private List<StartupListener> startupListeners = new ArrayList<StartupListener>();
74  
75      private List<DeploymentListener> deploymentListeners = new CopyOnWriteArrayList<DeploymentListener>();
76  
77      public DeploymentService()
78      {
79          deployer = new DefaultMuleDeployer(this);
80          appFactory = new ApplicationFactory(this);
81      }
82  
83      public void start()
84      {
85          // install phase
86          final Map<String, Object> options = StartupContext.get().getStartupOptions();
87          String appString = (String) options.get("app");
88  
89          final File appsDir = MuleContainerBootstrapUtils.getMuleAppsDir();
90  
91          // delete any leftover anchor files from previous unclean shutdowns
92          String[] appAnchors = appsDir.list(new SuffixFileFilter(APP_ANCHOR_SUFFIX));
93          for (String anchor : appAnchors)
94          {
95              // ignore result
96              new File(appsDir, anchor).delete();
97          }
98  
99          String[] apps = ArrayUtils.EMPTY_STRING_ARRAY;
100 
101         // mule -app app1:app2:app3 will restrict deployment only to those specified apps
102         final boolean explicitAppSet = appString != null;
103 
104         DeploymentStatusTracker deploymentStatusTracker = new DeploymentStatusTracker();
105         addDeploymentListener(deploymentStatusTracker);
106 
107         StartupSummaryDeploymentListener summaryDeploymentListener = new StartupSummaryDeploymentListener(deploymentStatusTracker);
108         addStartupListener(summaryDeploymentListener);
109 
110         if (!explicitAppSet)
111         {
112             String[] dirApps = appsDir.list(DirectoryFileFilter.DIRECTORY);
113             apps = (String[]) ArrayUtils.addAll(apps, dirApps);
114 
115             String[] zipApps = appsDir.list(ZIP_APPS_FILTER);
116             for (int i = 0; i < zipApps.length; i++)
117             {
118                 zipApps[i] = StringUtils.removeEndIgnoreCase(zipApps[i], ZIP_FILE_SUFFIX);
119             }
120 
121             // TODO this is a place to put a FQN of the custom sorter (use AND filter)
122             // Add string shortcuts for bundled ones
123             apps = (String[]) ArrayUtils.addAll(dirApps, zipApps);
124             Arrays.sort(apps);
125         }
126         else
127         {
128             apps = appString.split(":");
129         }
130 
131         apps = removeDuplicateAppNames(apps);
132 
133         for (String app : apps)
134         {
135             final Application a;
136             String appMarker = app;
137             File applicationFile = null;
138             try
139             {
140                 // if there's a zip, explode and install it
141                 applicationFile = new File(appsDir, app + ".zip");
142                 if (applicationFile.exists() && applicationFile.isFile())
143                 {
144                     appMarker = app + ZIP_FILE_SUFFIX;
145                     a = deployer.installFromAppDir(applicationFile.getName());
146                 }
147                 else
148                 {
149                     // otherwise just create an app object from a deployed app
150                     applicationFile = new File(appsDir, appMarker);
151                     a = appFactory.createApp(app);
152                 }
153                 applications.add(a);
154             }
155             catch (Throwable t)
156             {
157                 fireOnDeploymentFailure(appMarker, t);
158 
159                 try
160                 {
161                     URL url = applicationFile.toURI().toURL();
162                     addZombie(url);
163                 }
164                 catch (MalformedURLException e)
165                 {
166                     if (logger.isDebugEnabled())
167                     {
168                         logger.debug("Error adding zombie app", e);
169                     }
170                 }
171                 logger.error(String.format("Failed to create application [%s]", appMarker), t);
172             }
173         }
174 
175 
176         for (Application application : applications)
177         {
178             try
179             {
180                 fireOnDeploymentStart(application.getAppName());
181                 deployer.deploy(application);
182                 fireOnDeploymentSuccess(application.getAppName());
183             }
184             catch (Throwable t)
185             {
186                 fireOnDeploymentFailure(application.getAppName(), t);
187 
188                 // error text has been created by the deployer already
189                 final String msg = miniSplash(String.format("Failed to deploy app '%s', see below", application.getAppName()));
190                 logger.error(msg);
191                 logger.error(t);
192             }
193         }
194 
195         for (StartupListener listener : startupListeners)
196         {
197             try
198             {
199                 listener.onAfterStartup();
200             }
201             catch (Throwable t)
202             {
203                 logger.error(t);
204             }
205         }
206 
207         // only start the monitor thread if we launched in default mode without explicitly
208         // stated applications to launch
209         if (!explicitAppSet)
210         {
211             scheduleChangeMonitor(appsDir);
212         }
213         else
214         {
215             if (logger.isInfoEnabled())
216             {
217                 logger.info(miniSplash("Mule is up and running in a fixed app set mode"));
218             }
219         }
220     }
221 
222     private String[] removeDuplicateAppNames(String[] apps)
223     {
224         List<String> appNames = new LinkedList<String>();
225 
226         for (String appName : apps)
227         {
228             if (!appNames.contains(appName))
229             {
230                 appNames.add(appName);
231             }
232         }
233 
234         return appNames.toArray(ArrayUtils.EMPTY_STRING_ARRAY);
235     }
236 
237     protected void scheduleChangeMonitor(File appsDir)
238     {
239         final int reloadIntervalMs = DEFAULT_CHANGES_CHECK_INTERVAL_MS;
240         appDirMonitorTimer = Executors.newSingleThreadScheduledExecutor(new AppDeployerMonitorThreadFactory());
241 
242         appDirMonitorTimer.scheduleWithFixedDelay(new AppDirWatcher(appsDir),
243                                                   0,
244                                                   reloadIntervalMs,
245                                                   TimeUnit.MILLISECONDS);
246 
247         if (logger.isInfoEnabled())
248         {
249             logger.info(miniSplash(String.format("Mule is up and kicking (every %dms)", reloadIntervalMs)));
250         }
251     }
252 
253     public void stop()
254     {
255         if (appDirMonitorTimer != null)
256         {
257             appDirMonitorTimer.shutdownNow();
258         }
259 
260         // tear down apps in reverse order
261         Collections.reverse(applications);
262         for (Application application : applications)
263         {
264             try
265             {
266                 application.stop();
267                 application.dispose();
268             }
269             catch (Throwable t)
270             {
271                 logger.error(t);
272             }
273         }
274 
275     }
276 
277     /**
278      * Find an active application.
279      * @return null if not found
280      */
281     public Application findApplication(String appName)
282     {
283         return (Application) CollectionUtils.find(applications, new BeanPropertyValueEqualsPredicate("appName", appName));
284     }
285 
286     /**
287      * @return immutable applications list
288      */
289     public List<Application> getApplications()
290     {
291         return Collections.unmodifiableList(applications);
292     }
293 
294     /**
295      * @return URL/lastModified of apps which previously failed to deploy
296      */
297     public Map<URL, Long> getZombieMap()
298     {
299         return zombieMap;
300     }
301 
302     protected MuleDeployer getDeployer()
303     {
304         return deployer;
305     }
306 
307     public void setDeployer(MuleDeployer deployer)
308     {
309         this.deployer = deployer;
310     }
311 
312     public void setAppFactory(ApplicationFactory appFactory)
313     {
314         this.appFactory = appFactory;
315     }
316 
317     public ApplicationFactory getAppFactory()
318     {
319         return appFactory;
320     }
321 
322     public ReentrantLock getLock() {
323         return lock;
324     }
325 
326     public void onApplicationInstalled(Application a)
327     {
328         applications.add(a);
329     }
330 
331     protected void undeploy(Application app)
332     {
333         if (logger.isInfoEnabled())
334         {
335             logger.info("================== Request to Undeploy Application: " + app.getAppName());
336         }
337 
338         applications.remove(app);
339         deployer.undeploy(app);
340     }
341 
342     public void undeploy(String appName)
343     {
344         Application app = (Application) CollectionUtils.find(applications, new BeanPropertyValueEqualsPredicate("appName", appName));
345         undeploy(app);
346     }
347 
348     public void deploy(URL appArchiveUrl) throws IOException
349     {
350         final Application application;
351         try
352         {
353             application = deployer.installFrom(appArchiveUrl);
354             applications.add(application);
355             deployer.deploy(application);
356         }
357         catch (Throwable t)
358         {
359             addZombie(appArchiveUrl);
360             if (t instanceof DeploymentException)
361             {
362                 // re-throw
363                 throw ((DeploymentException) t);
364             }
365 
366             final String msg = "Failed to deploy from URL: " + appArchiveUrl;
367             throw new DeploymentException(MessageFactory.createStaticMessage(msg), t);
368         }
369     }
370 
371     protected void addZombie(URL appArchiveUrl)
372     {
373         // no sync required as deploy operations are single-threaded
374         if (appArchiveUrl == null)
375         {
376             return;
377         }
378 
379         long lastModified = FileUtils.getFileTimeStamp(appArchiveUrl);
380 
381         zombieMap.put(appArchiveUrl, lastModified);
382     }
383 
384     public void addStartupListener(StartupListener listener)
385     {
386         this.startupListeners.add(listener);
387     }
388 
389     public void removeStartupListener(StartupListener listener)
390     {
391         this.startupListeners.remove(listener);
392     }
393 
394     public void addDeploymentListener(DeploymentListener listener)
395     {
396         this.deploymentListeners.add(listener);
397     }
398 
399     public void removeDeploymentListener(DeploymentListener listener)
400     {
401         this.deploymentListeners.remove(listener);
402     }
403 
404     /**
405      * Notifies all deployment listeners that the deploy for a given application
406      * has just started.
407      *
408      * @param appName the name of the application being deployed.
409      */
410     protected void fireOnDeploymentStart(String appName)
411     {
412         for (DeploymentListener listener : deploymentListeners)
413         {
414             try
415             {
416                 listener.onDeploymentStart(appName);
417             }
418             catch (Throwable t)
419             {
420                 logger.error("Listener failed to process onDeploymentStart notification", t);
421             }
422         }
423     }
424 
425     /**
426      * Notifies all deployment listeners that the deploy for a given application
427      * has successfully finished.
428      *
429      * @param appName the name of the deployed application.
430      */
431     protected void fireOnDeploymentSuccess(String appName)
432     {
433         for (DeploymentListener listener : deploymentListeners)
434         {
435             try
436             {
437                 listener.onDeploymentSuccess(appName);
438             }
439             catch (Throwable t)
440             {
441                 logger.error("Listener failed to process onDeploymentSuccess notification", t);
442             }
443         }
444     }
445 
446     /**
447      * Notifies all deployment listeners that the deploy for a given application
448      * has finished with a failure.
449      *
450      * @param appName the name of the deployed application.
451      * @param cause the cause of the deployment failure.
452      */
453     protected void fireOnDeploymentFailure(String appName, Throwable cause)
454     {
455         for (DeploymentListener listener : deploymentListeners)
456         {
457             try
458             {
459                 listener.onDeploymentFailure(appName, cause);
460             }
461             catch (Throwable t)
462             {
463                 logger.error("Listener failed to process onDeploymentFailure notification", t);
464             }
465         }
466     }
467 
468     public interface StartupListener
469     {
470 
471         /**
472          * Invoked after all apps have passed the deployment phase. Any exceptions thrown by implementations
473          * will be ignored.
474          */
475         void onAfterStartup();
476     }
477 
478     /**
479      * Not thread safe. Correctness is guaranteed by a single-threaded executor.
480      */
481     protected class AppDirWatcher implements Runnable
482     {
483         protected File appsDir;
484 
485         protected volatile boolean dirty;
486 
487         public AppDirWatcher(final File appsDir)
488         {
489             this.appsDir = appsDir;
490             applications.addPropertyChangeListener(new PropertyChangeListener()
491             {
492                 public void propertyChange(PropertyChangeEvent e)
493                 {
494                     if (e instanceof ElementAddedEvent || e instanceof ElementRemovedEvent)
495                     {
496                         if (logger.isDebugEnabled())
497                         {
498                             logger.debug("Deployed applications set has been modified, flushing state.");
499                         }
500                         dirty = true;
501                     }
502                 }
503             });
504         }
505 
506         // Cycle is:
507         //   undeploy removed apps
508         //   deploy archives
509         //   deploy exploded
510         public void run()
511         {
512             try
513             {
514                 if (logger.isDebugEnabled())
515                 {
516                     logger.debug("Checking for changes...");
517                 }
518                 // use non-barging lock to preserve fairness, according to javadocs
519                 // if there's a lock present - wait for next poll to do anything
520                 if (!lock.tryLock(0, TimeUnit.SECONDS))
521                 {
522                     if (logger.isDebugEnabled())
523                     {
524                         logger.debug("Another deployment operation in progress, will skip this cycle. Owner thread: " +
525                                      ((DebuggableReentrantLock) lock).getOwner());
526                     }
527                     return;
528                 }
529 
530                 // list new apps
531                 final String[] zips = appsDir.list(ZIP_APPS_FILTER);
532                 String[] apps = appsDir.list(DirectoryFileFilter.DIRECTORY);
533 
534                 // we care only about removed anchors
535                 String[] currentAnchors = appsDir.list(new SuffixFileFilter(APP_ANCHOR_SUFFIX));
536                 if (logger.isDebugEnabled())
537                 {
538                     StringBuilder sb = new StringBuilder();
539                     sb.append(String.format("Current anchors:%n"));
540                     for (String currentAnchor : currentAnchors)
541                     {
542                         sb.append(String.format("  %s%n", currentAnchor));
543                     }
544                     logger.debug(sb.toString());
545                 }
546 
547                 String[] appAnchors = findExpectedAnchorFiles();
548 
549                 @SuppressWarnings("unchecked")
550                 final Collection<String> deletedAnchors = CollectionUtils.subtract(Arrays.asList(appAnchors), Arrays.asList(currentAnchors));
551                 if (logger.isDebugEnabled())
552                 {
553                     StringBuilder sb = new StringBuilder();
554                     sb.append(String.format("Deleted anchors:%n"));
555                     for (String deletedAnchor : deletedAnchors)
556                     {
557                         sb.append(String.format("  %s%n", deletedAnchor));
558                     }
559                     logger.debug(sb.toString());
560                 }
561 
562                 for (String deletedAnchor : deletedAnchors)
563                 {
564                     String appName = StringUtils.removeEnd(deletedAnchor, APP_ANCHOR_SUFFIX);
565                     try
566                     {
567                         if (findApplication(appName) != null)
568                         {
569                             undeploy(appName);
570                         }
571                         else if (logger.isDebugEnabled())
572                         {
573                             logger.debug(String.format("Application [%s] has already been undeployed via API", appName));
574                         }
575                     }
576                     catch (Throwable t)
577                     {
578                         logger.error("Failed to undeploy application: " + appName, t);
579                     }
580                 }
581 
582                 // new packed Mule apps
583                 for (String zip : zips)
584                 {
585                     URL url = null;
586                     try
587                     {
588                         // check if this app is running first, undeploy it then
589                         final String appName = StringUtils.removeEnd(zip, ".zip");
590                         Application app = (Application) CollectionUtils.find(applications, new BeanPropertyValueEqualsPredicate("appName", appName));
591                         if (app != null)
592                         {
593                             undeploy(appName);
594                         }
595                         url = new File(appsDir, zip).toURI().toURL();
596 
597                         if (isZombieApplicationFile(url))
598                         {
599                             // Skips the file because it was already deployed with failure
600                             continue;
601                         }
602 
603                         deploy(url);
604                     }
605                     catch (Throwable t)
606                     {
607                         logger.error("Failed to deploy application archive: " + zip, t);
608                         addZombie(url);
609                     }
610                 }
611 
612                 // re-scan exploded apps and update our state, as deploying Mule app archives might have added some
613                 if (zips.length > 0 || dirty)
614                 {
615                     apps = appsDir.list(DirectoryFileFilter.DIRECTORY);
616                 }
617 
618                 Collection deployedAppNames = CollectionUtils.collect(applications, new BeanToPropertyValueTransformer("appName"));
619 
620                 // new exploded Mule apps
621                 @SuppressWarnings("unchecked")
622                 final Collection<String> addedApps = CollectionUtils.subtract(Arrays.asList(apps), deployedAppNames);
623                 for (String addedApp : addedApps)
624                 {
625                     try
626                     {
627                         onNewExplodedApplication(addedApp);
628                     }
629                     catch (Throwable t)
630                     {
631                         logger.error("Failed to deploy exploded application: " + addedApp, t);
632                         try
633                         {
634                             addZombie(new File(appsDir, addedApp).toURI().toURL());
635                         }
636                         catch (MalformedURLException e)
637                         {
638                             if (logger.isDebugEnabled())
639                             {
640                                 logger.debug(e);
641                             }
642                         }
643                     }
644                 }
645 
646             }
647             catch (InterruptedException e)
648             {
649                 // preserve the flag for the thread
650                 Thread.currentThread().interrupt();
651             }
652             finally
653             {
654                 if (lock.isHeldByCurrentThread())
655                 {
656                     lock.unlock();
657                 }
658                 dirty = false;
659             }
660         }
661 
662         /**
663          * Returns the list of anchor file names for the deployed apps
664          *
665          * @return a non null list of file names
666          */
667         private String[] findExpectedAnchorFiles()
668         {
669             String[] appAnchors = new String[applications.size()];
670             int i =0;
671             for (Application application : applications)
672             {
673                 appAnchors[i++] = application.getAppName() + APP_ANCHOR_SUFFIX;
674             }
675             return appAnchors;
676         }
677 
678         /**
679          * Determines if a given URL points to the same file that an existent
680          * zombie application.
681          *
682          * @param url the URL representing the resource to be checked
683          * @return true if the URL already a zombie application and both file
684          *         timestamps are the same.
685          */
686         protected boolean isZombieApplicationFile(URL url)
687         {
688             boolean result = false;
689 
690             if (FileUtils.isFile(url) && zombieMap.containsKey(url))
691             {
692                 long originalTimeStamp = zombieMap.get(url);
693                 long newTimeStamp = FileUtils.getFileTimeStamp(url);
694 
695                 if (originalTimeStamp == newTimeStamp)
696                 {
697                     result = true;
698                 }
699             }
700 
701             return result;
702         }
703 
704         /**
705          * @param appName application name as it appears in $MULE_HOME/apps
706          */
707         protected void onNewExplodedApplication(String appName) throws Exception
708         {
709             if (logger.isInfoEnabled())
710             {
711                 logger.info("================== New Exploded Application: " + appName);
712             }
713 
714             Application a = appFactory.createApp(appName);
715             // add to the list of known apps first to avoid deployment loop on failure
716             onApplicationInstalled(a);
717             deployer.deploy(a);
718         }
719 
720     }
721 }