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