View Javadoc

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