View Javadoc

1   package com.atlassian.sal.core.lifecycle;
2   
3   import java.util.ArrayList;
4   import java.util.Collection;
5   import java.util.Collections;
6   import java.util.HashSet;
7   import java.util.List;
8   import java.util.Set;
9   
10  import javax.annotation.PostConstruct;
11  import javax.annotation.PreDestroy;
12  
13  import com.atlassian.plugin.PluginAccessor;
14  import com.atlassian.plugin.event.PluginEventListener;
15  import com.atlassian.plugin.event.PluginEventManager;
16  import com.atlassian.plugin.event.events.PluginEnabledEvent;
17  import com.atlassian.plugin.event.events.PluginFrameworkStartedEvent;
18  import com.atlassian.sal.api.lifecycle.LifecycleAware;
19  import com.atlassian.sal.api.lifecycle.LifecycleManager;
20  
21  import org.osgi.framework.Bundle;
22  import org.osgi.framework.BundleContext;
23  import org.osgi.framework.ServiceEvent;
24  import org.osgi.framework.ServiceListener;
25  import org.osgi.framework.ServiceReference;
26  import org.slf4j.Logger;
27  import org.slf4j.LoggerFactory;
28  import org.springframework.beans.factory.DisposableBean;
29  import org.springframework.beans.factory.InitializingBean;
30  
31  import static com.atlassian.plugin.osgi.util.OsgiHeaderUtil.getPluginKey;
32  import static org.osgi.framework.Constants.OBJECTCLASS;
33  
34  /**
35   * Manage the tracking of {@link LifecycleAware} services registered to OSGi and call their {@link LifecycleAware#onStart} when
36   * required.
37   * <p/>
38   * The {@code onStart} method is called when the application is setup according to {@link LifecycleManager#isApplicationSetUp}, the
39   * plugin is enabled, and the plugin framework has started.
40   * <p/>
41   * This class has concurrency controls for the case of concurrent service registration/deregistration with plugin and application
42   * state activity. It does not account for concurrency of plugin notifications against themselves. This does not happen during
43   * normal usage.
44   */
45  public abstract class DefaultLifecycleManager implements LifecycleManager, InitializingBean, DisposableBean
46  {
47      private static final Logger log = LoggerFactory.getLogger(DefaultLifecycleManager.class);
48  
49      private final PluginEventManager pluginEventManager;
50      private final PluginAccessor pluginAccessor;
51      private final BundleContext bundleContext;
52  
53      /**
54       * The LifecycleAware services which we know about via OSGi notifications, but which we have not yet called onStart() on.
55       * <p/>
56       * The documentation for {@link org.osgi.framework.ServiceReference} is quite explicit that while a framework may return
57       * multiple ServiceReference objects referring to the same ServiceRegistration, all such ServiceReference objects are equals()
58       * and have the same hashCode(), so it is safe to use them in a set like this.
59       * <p/>
60       * Inspection of Felix code (as at 4.2.1 of this writing) shows that in fact Felix is careful to have only a single
61       * ServiceReference for each ServiceRegistration. If we move to a framework where this is not so, or if this causes problems,
62       * we'd need to replace this with a map from the ServiceReference.getProperty(SERVICE_ID) to ServiceReference so we can key
63       * service tracking off SERVICE_ID.
64       */
65      private final Set<ServiceReference<LifecycleAware>> pendingLifecycleAwares;
66      private final ServiceListener serviceListener;
67  
68      private boolean started;
69  
70      public DefaultLifecycleManager(
71              final PluginEventManager pluginEventManager,
72              final PluginAccessor pluginAccessor,
73              final BundleContext bundleContext)
74      {
75          this.pluginEventManager = pluginEventManager;
76          this.pluginAccessor = pluginAccessor;
77          this.bundleContext = bundleContext;
78          this.pendingLifecycleAwares = Collections.synchronizedSet(new HashSet<ServiceReference<LifecycleAware>>());
79          this.serviceListener = new LifecycleAwareServiceListener();
80          this.started = false;
81      }
82  
83      @Override
84      public void afterPropertiesSet() throws Exception
85      {
86          pluginEventManager.register(this);
87          // Gather all the current LifecycleAware instances, and watch for new ones
88          final String filter = "(" + OBJECTCLASS + "=" + LifecycleAware.class.getName() + ")";
89          // Since OSGi doesn't (afaics) provide an atomic way to add a ServiceListener and get brought up to date, we need to be a
90          // bit careful here. If we miss an add, we won't call an onStart() we should, and if we miss a remove we'll possibly leak.
91          // So we starting listening, add everything we can see, and discard anything invalid.
92  
93          // An an aside, add everything, add listener, add everything doesn't work - a service that is removed after the snapshot
94          // for the second add everything is readded when this snapshot is added, and so you end up missing a remove.
95  
96          // Services that come and go here before this point are irrelevant
97          bundleContext.addServiceListener(serviceListener, filter);
98          // Anything added from now on is captured, so we don't lose services
99          final Collection<ServiceReference<LifecycleAware>> services =
100                 bundleContext.getServiceReferences(LifecycleAware.class, null);
101         // Our snapshot may contain services that have already made it to pending. This means we might get double adds (which the
102         // set takes care of), or we might re-add something in our snapshot that gets removed in this window. Such a service might
103         // have turned up before we started listening or after, so the listener might think the remove is spurious or normal, but
104         // the service is in our snapshot.
105         pendingLifecycleAwares.addAll(services);
106         // Any remove from now on is correctly processed - we've gathered everything and put it it pendingLifecycleAwares. However,
107         // we might have re-added some service that was removed post snapshot but before we updated pendingLifecycleAwares. To deal
108         // with these stale services, we do a sweep and chuck them out.  We can sweep the services from the getServiceReferences
109         // snapshot, because those that arrived via the listener will be present when removed. A service added before the listener
110         // is added but removed afterwards, but before the snapshot, is just ignored as a spurious remove.
111         for (final ServiceReference<LifecycleAware> service : services)
112         {
113             // The documentation for getBundle() says this is a sanctioned way to detect deregistration
114             if (null == service.getBundle())
115             {
116                 pendingLifecycleAwares.remove(service);
117             }
118         }
119     }
120 
121     @Override
122     public void destroy() throws Exception
123     {
124         bundleContext.removeServiceListener(serviceListener);
125         // We  clear out our pendingLifecycleAwares, to assist garbage collection. This is just a heuristic - if this code moves
126         // this is *not* sufficient to prevent leaks, since a thread could be in serviceListener now, having arrived before we
127         // removed the listener but not yet finished. Since this is a @PreDestroy and things are going away, the GC will save us.
128         // If we do move this code, we should probably move pendingLifecycleAwares to the serviceListener and have their lifetimes
129         // match, but I'm not going to pre-emptively solve this synchronization problem without knowledge of the lifetimes and where
130         // things are moving to.
131         pendingLifecycleAwares.clear();
132         pluginEventManager.unregister(this);
133     }
134 
135     @PluginEventListener
136     public void onPluginFrameworkStarted(final PluginFrameworkStartedEvent event)
137     {
138         startIfApplicationSetup();
139     }
140 
141     @PluginEventListener
142     public void onPluginEnabled(final PluginEnabledEvent event)
143     {
144         // I don't think it's actually worth restricting to the "right" plugin here - if a whole lot of stuff is enabled when
145         // we get the event, we may as well start them all in one hit. This will stop lots of doubling handling of outstanding
146         // LifecycleAware instances. However, this is called a lot before startup, so we hoist the started check.
147         if (started)
148         {
149             notifyStartableLifecycleAwares();
150         }
151     }
152 
153     public void start()
154     {
155         startIfApplicationSetup();
156     }
157 
158     private void startIfApplicationSetup()
159     {
160         final boolean doSetup = !started && isApplicationSetUp();
161         if (doSetup)
162         {
163             started = true;
164             // We could call the internal notify irrespective of doSetup, since it guards internally, but since we need to guard
165             // for subclasses, we may as well hoist this in here and save a bit of effort in the no-op case.
166             notifyStartableLifecycleAwares();
167             notifyOnStart();
168         }
169     }
170 
171     /**
172      * An override point for subclasses that require callback notification after the initial start of the system.
173      */
174     protected void notifyOnStart()
175     {
176     }
177 
178     private void notifyStartableLifecycleAwares()
179     {
180         // Grab a copy to operate on, so we don't call onStart() holding a lock. We're using toArray here to keep the
181         // synchronization clear at the cost of the unchecked cast below. If we went via ImmutableList.copyOf, we'd need to either
182         // synchronize externally or depend on the implementation of copyOf - see Collections.synchronizedSet re iteration.
183         final Object[] pending = pendingLifecycleAwares.toArray();
184 
185         // Notify those we can, remembering what we handled.
186         final List<ServiceReference<LifecycleAware>> completed = new ArrayList<ServiceReference<LifecycleAware>>(pending.length);
187         for (final Object serviceRaw : pending)
188         {
189             @SuppressWarnings("unchecked")
190             // serviceRaw comes from Object[] pending which is a snapshot of pendingLifecycleAwares so the cast is safe
191             final ServiceReference<LifecycleAware> service = (ServiceReference<LifecycleAware>) serviceRaw;
192             if (notifyLifecycleAwareIfStartedAndEnabled(service))
193             {
194                 completed.add(service);
195             }
196         }
197         // Now remove those we handled.
198         pendingLifecycleAwares.removeAll(completed);
199     }
200 
201     /**
202      * Notify a LifecycleAware if the system is started and the plugin publishing the service is enabled.
203      *
204      * @param service the service to attempt to start
205      * @return true iff the service was started or is stale, false if it should be retained for later processing.
206      */
207     private boolean notifyLifecycleAwareIfStartedAndEnabled(final ServiceReference<LifecycleAware> service)
208     {
209         if (started)
210         {
211             final Bundle bundle = service.getBundle();
212             final LifecycleAware lifecycleAware = bundleContext.getService(service);
213             try
214             {
215                 // Either of bundle or lifecycleAware can be null if the service isn't registered.
216                 if ((null != bundle) && (null != lifecycleAware))
217                 {
218                     final String pluginKey = getPluginKey(bundle);
219                     if (pluginAccessor.isPluginEnabled(pluginKey))
220                     {
221                         try
222                         {
223                             log.debug("Calling LifecycleAware.onStart() '{}' from plugin '{}'", lifecycleAware, pluginKey);
224                             lifecycleAware.onStart();
225                         }
226                         catch (final RuntimeException ex)
227                         {
228                             log.error("LifecycleAware.onStart() failed for component with class '{}' from plugin '{}'",
229                                     new Object[] { lifecycleAware.getClass().getName(), pluginKey, ex });
230                         }
231                         // We have called onStart
232                         return true;
233                     }
234                     else
235                     {
236                         // We have not called onStart and need to later (when the plugin enables)
237                         return false;
238                     }
239                 }
240                 else
241                 {
242                     // This can happen if we take our snapshot of services to notify, and then one is unregistered. In this case,
243                     // likely both bundle and lifecycleAware are null, so there's not much we can get in the message here, and it means
244                     // niceties like trying to safely dig data out of them is probably not worth a whole lot.
245                     log.warn("Discarding onStart() for stale LifecycleAware");
246                     // This reference is stale, so we report true as it can be discarded
247                     return true;
248                 }
249             }
250             finally
251             {
252                 if (null != lifecycleAware)
253                 {
254                     bundleContext.ungetService(service);
255                 }
256             }
257         }
258         else
259         {
260             // We have not called onStart and need to later (when the system is started)
261             return false;
262         }
263     }
264 
265     private class LifecycleAwareServiceListener implements ServiceListener
266     {
267         /**
268          * Handle a service changed by updating the list of pendingLifecycleAwares or dispatching onStart as necessary.
269          *
270          * @param serviceEvent details of the changed service
271          */
272         @Override
273         public void serviceChanged(final ServiceEvent serviceEvent)
274         {
275             @SuppressWarnings("unchecked")
276             // The cast here is safe since we filter by this object class when adding the service listener
277             final ServiceReference<LifecycleAware> service = (ServiceReference<LifecycleAware>) serviceEvent.getServiceReference();
278             switch (serviceEvent.getType())
279             {
280                 case ServiceEvent.REGISTERED:
281                 {
282                     if (!notifyLifecycleAwareIfStartedAndEnabled(service))
283                     {
284                         // We didn't satisfy notification conditions, so hang on to the service for later.
285                         pendingLifecycleAwares.add(service);
286                     }
287                     // else we tried to onStart it (we're started and the plugin is enabled), or it's stale. Either way, we're done.
288                     break;
289                 }
290                 case ServiceEvent.UNREGISTERING:
291                 {
292                     // It's possible we still have a reference to the service, for example if the plugin got as far as
293                     // registering the service before failing, and hence never became enabled. See the comment on the
294                     // pendingLifecycleAwares field for an explanation of why removing the ServiceReference directly is safe.
295                     pendingLifecycleAwares.remove(service);
296                     break;
297                 }
298                 default:
299                 {
300                     // We filter on objectClass, which org.osgi.framework.ServiceRegistration.setProperties() says cannot be
301                     // changed dynamically, so we don't need to handle MODIFIED or MODIFIED_ENDMATCH.
302                     break;
303                 }
304             }
305         }
306     }
307 }