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