View Javadoc

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  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      protected transient final Log logger = LogFactory.getLog(getClass());
57      protected MuleDeployer deployer;
58      protected ApplicationFactory appFactory;
59      // fair lock
60      private ReentrantLock lock = new DebuggableReentrantLock(true);
61  
62      private ObservableList<Application> applications = new ObservableList<Application>();
63      private Map<URL, Long> zombieMap = new HashMap<URL, Long>();
64      private ObservableList<URL> zombieLand = new ObservableList<URL>();
65  
66      public DeploymentService()
67      {
68          deployer = new DefaultMuleDeployer(this);
69          appFactory = new ApplicationFactory(this);
70      }
71  
72      public void start()
73      {
74          // install phase
75          final Map<String, Object> options = StartupContext.get().getStartupOptions();
76          String appString = (String) options.get("app");
77  
78          final File appsDir = MuleContainerBootstrapUtils.getMuleAppsDir();
79  
80          // delete any leftover anchor files from previous unclean shutdowns
81          String[] appAnchors = appsDir.list(new SuffixFileFilter(APP_ANCHOR_SUFFIX));
82          for (String anchor : appAnchors)
83          {
84              // ignore result
85              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          final boolean explicitAppSet = appString != null;
92  
93          if (!explicitAppSet)
94          {
95              // explode any app zips first
96              final String[] zips = appsDir.list(new SuffixFileFilter(".zip"));
97              Arrays.sort(zips);
98              for (String zip : zips)
99              {
100                 try
101                 {
102                     // we don't care about the returned app object on startup
103                     deployer.installFromAppDir(zip);
104                 }
105                 catch (Throwable t)
106                 {
107                     logger.error(String.format("Failed to install app from archive '%s'", zip), t);
108                     File appFile = new File(appsDir, zip);
109                     try
110                     {
111                         addZombie(appFile.toURL());
112                     }
113                     catch (MalformedURLException muex)
114                     {
115                         if (logger.isDebugEnabled())
116                         {
117                             logger.debug(muex);
118                         }
119                     }
120                 }
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             apps = appsDir.list(DirectoryFileFilter.DIRECTORY);
126             Arrays.sort(apps);
127         }
128         else
129         {
130             apps = appString.split(":");
131         }
132 
133         for (String app : apps)
134         {
135             final Application a;
136             try
137             {
138                 a = appFactory.createApp(app);
139                 applications.add(a);
140             }
141             catch (IOException e)
142             {
143                 // TODO logging
144                 e.printStackTrace();
145             }
146         }
147 
148 
149         for (Application application : applications)
150         {
151             try
152             {
153                 deployer.deploy(application);
154             }
155             catch (Throwable t)
156             {
157                 // TODO logging
158                 t.printStackTrace();
159             }
160         }
161 
162         // only start the monitor thread if we launched in default mode without explicitly
163         // stated applications to launch
164         if (!explicitAppSet)
165         {
166             scheduleChangeMonitor(appsDir);
167         }
168     }
169 
170     protected void scheduleChangeMonitor(File appsDir)
171     {
172         final int reloadIntervalMs = DEFAULT_CHANGES_CHECK_INTERVAL_MS;
173         appDirMonitorTimer = Executors.newSingleThreadScheduledExecutor(new AppDeployerMonitorThreadFactory());
174 
175         appDirMonitorTimer.scheduleWithFixedDelay(new AppDirWatcher(appsDir),
176                                                   0,
177                                                   reloadIntervalMs,
178                                                   TimeUnit.MILLISECONDS);
179 
180         if (logger.isInfoEnabled())
181         {
182             logger.info("Application directory check interval: " + reloadIntervalMs);
183         }
184     }
185 
186     public void stop()
187     {
188         if (appDirMonitorTimer != null)
189         {
190             appDirMonitorTimer.shutdownNow();
191         }
192 
193         // tear down apps in reverse order
194         Collections.reverse(applications);
195         for (Application application : applications)
196         {
197             try
198             {
199                 application.stop();
200                 application.dispose();
201             }
202             catch (Throwable t)
203             {
204                 // TODO logging
205                 t.printStackTrace();
206             }
207         }
208 
209     }
210 
211     /**
212      * Find an active application.
213      * @return null if not found
214      */
215     public Application findApplication(String appName)
216     {
217         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         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         return zombieMap;
234     }
235 
236 
237 
238     protected MuleDeployer getDeployer()
239     {
240         return deployer;
241     }
242 
243     public void setDeployer(MuleDeployer deployer)
244     {
245         this.deployer = deployer;
246     }
247 
248     public ApplicationFactory getAppFactory()
249     {
250         return appFactory;
251     }
252 
253     public ReentrantLock getLock() {
254         return lock;
255     }
256 
257     public void onApplicationInstalled(Application a)
258     {
259         applications.add(a);
260     }
261 
262     protected void undeploy(Application app)
263     {
264         if (logger.isInfoEnabled())
265         {
266             logger.info("================== Request to Undeploy Application: " + app.getAppName());
267         }
268 
269         deployer.undeploy(app);
270     }
271 
272     public void undeploy(String appName)
273     {
274         Application app = (Application) CollectionUtils.find(applications, new BeanPropertyValueEqualsPredicate("appName", appName));
275         applications.remove(app);
276         deployer.undeploy(app);
277     }
278 
279     public void deploy(URL appArchiveUrl) throws IOException
280     {
281         final Application application;
282         try
283         {
284             application = deployer.installFrom(appArchiveUrl);
285             applications.add(application);
286             deployer.deploy(application);
287         }
288         catch (Throwable t)
289         {
290             addZombie(appArchiveUrl);
291             if (t instanceof DeploymentException)
292             {
293                 // re-throw
294                 throw ((DeploymentException) t);
295             }
296 
297             final String msg = "Failed to deploy from URL: " + appArchiveUrl;
298             throw new DeploymentException(MessageFactory.createStaticMessage(msg), t);
299         }
300     }
301 
302     protected void addZombie(URL appArchiveUrl)
303     {
304         // no sync required as deploy operations are single-threaded
305         if (appArchiveUrl == null)
306         {
307             return;
308         }
309 
310         long lastModified = -1;
311         // get timestamp only from file:// urls
312         if ("file".equals(appArchiveUrl.getProtocol()))
313         {
314             lastModified = new File(appArchiveUrl.getFile()).lastModified();
315         }
316 
317         zombieMap.put(appArchiveUrl, lastModified);
318     }
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         protected String[] appAnchors = new String[0];
329         protected volatile boolean dirty;
330 
331         public AppDirWatcher(final File appsDir)
332         {
333             this.appsDir = appsDir;
334             applications.addPropertyChangeListener(new PropertyChangeListener()
335             {
336                 public void propertyChange(PropertyChangeEvent e)
337                 {
338                     if (e instanceof ElementAddedEvent || e instanceof ElementRemovedEvent)
339                     {
340                         if (logger.isDebugEnabled())
341                         {
342                             logger.debug("Deployed applications set has been modified, flushing state.");
343                         }
344                         dirty = true;
345                     }
346                 }
347             });
348         }
349 
350         // Cycle is:
351         //   undeploy removed apps
352         //   deploy archives
353         //   deploy exploded
354         public void run()
355         {
356             try
357             {
358                 if (logger.isDebugEnabled())
359                 {
360                     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                 if (!lock.tryLock(0, TimeUnit.SECONDS))
365                 {
366                     if (logger.isDebugEnabled())
367                     {
368                         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                 final String[] zips = appsDir.list(new SuffixFileFilter(".zip"));
377                 String[] apps = appsDir.list(DirectoryFileFilter.DIRECTORY);
378 
379 
380                 // we care only about removed anchors
381                 String[] currentAnchors = appsDir.list(new SuffixFileFilter(APP_ANCHOR_SUFFIX));
382                 if (logger.isDebugEnabled())
383                 {
384                     StringBuilder sb = new StringBuilder();
385                     sb.append(String.format("Current anchors:%n"));
386                     for (String currentAnchor : currentAnchors)
387                     {
388                         sb.append(String.format("  %s%n", currentAnchor));
389                     }
390                     logger.debug(sb.toString());
391                 }
392                 @SuppressWarnings("unchecked")
393                 final Collection<String> deletedAnchors = CollectionUtils.subtract(Arrays.asList(appAnchors), Arrays.asList(currentAnchors));
394                 if (logger.isDebugEnabled())
395                 {
396                     StringBuilder sb = new StringBuilder();
397                     sb.append(String.format("Deleted anchors:%n"));
398                     for (String deletedAnchor : deletedAnchors)
399                     {
400                         sb.append(String.format("  %s%n", deletedAnchor));
401                     }
402                     logger.debug(sb.toString());
403                 }
404 
405                 for (String deletedAnchor : deletedAnchors)
406                 {
407                     String appName = StringUtils.removeEnd(deletedAnchor, APP_ANCHOR_SUFFIX);
408                     try
409                     {
410                         if (findApplication(appName) != null)
411                         {
412                             undeploy(appName);
413                         }
414                         else if (logger.isDebugEnabled())
415                         {
416                             logger.debug(String.format("Application [%s] has already been undeployed via API", appName));
417                         }
418                     }
419                     catch (Throwable t)
420                     {
421                         logger.error("Failed to undeploy application: " + appName, t);
422                     }
423                 }
424                 appAnchors = currentAnchors;
425 
426 
427                 // new packed Mule apps
428                 for (String zip : zips)
429                 {
430                     URL url = null;
431                     try
432                     {
433                         // check if this app is running first, undeploy it then
434                         final String appName = StringUtils.removeEnd(zip, ".zip");
435                         Application app = (Application) CollectionUtils.find(applications, new BeanPropertyValueEqualsPredicate("appName", appName));
436                         if (app != null)
437                         {
438                             undeploy(appName);
439                         }
440                         url = new File(appsDir, zip).toURI().toURL();
441                         deploy(url);
442                     }
443                     catch (Throwable t)
444                     {
445                         logger.error("Failed to deploy application archive: " + zip, t);
446                         addZombie(url);
447                     }
448                 }
449 
450                 // re-scan exploded apps and update our state, as deploying Mule app archives might have added some
451                 if (zips.length > 0 || dirty)
452                 {
453                     apps = appsDir.list(DirectoryFileFilter.DIRECTORY);
454                 }
455 
456                 Collection deployedAppNames = CollectionUtils.collect(applications, new BeanToPropertyValueTransformer("appName"));
457 
458                 // new exploded Mule apps
459                 @SuppressWarnings("unchecked")
460                 final Collection<String> addedApps = CollectionUtils.subtract(Arrays.asList(apps), deployedAppNames);
461                 for (String addedApp : addedApps)
462                 {
463                     try
464                     {
465                         onNewExplodedApplication(addedApp);
466                     }
467                     catch (Throwable t)
468                     {
469                         logger.error("Failed to deploy exploded application: " + addedApp, t);
470                         try
471                         {
472                             addZombie(new File(appsDir, addedApp).toURI().toURL());
473                         }
474                         catch (MalformedURLException e)
475                         {
476                             if (logger.isDebugEnabled())
477                             {
478                                 logger.debug(e);
479                             }
480                         }
481                     }
482                 }
483 
484             }
485             catch (InterruptedException e)
486             {
487                 // preserve the flag for the thread
488                 Thread.currentThread().interrupt();
489             }
490             finally
491             {
492                 if (lock.isHeldByCurrentThread())
493                 {
494                     lock.unlock();
495                 }
496                 dirty = false;
497             }
498         }
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             if (logger.isInfoEnabled())
506             {
507                 logger.info("================== New Exploded Application: " + appName);
508             }
509 
510             Application a = appFactory.createApp(appName);
511             // add to the list of known apps first to avoid deployment loop on failure
512             onApplicationInstalled(a);
513             deployer.deploy(a);
514         }
515 
516     }
517 
518 }