7ad8ddb4e3a5e4b9fd83096d99344cff4a257a06
[controller.git] / opendaylight / blueprint / src / main / java / org / opendaylight / controller / blueprint / BlueprintBundleTracker.java
1 /*
2  * Copyright (c) 2016 Brocade Communications Systems, Inc. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.controller.blueprint;
9
10 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
11 import java.util.ArrayList;
12 import java.util.Arrays;
13 import java.util.Collection;
14 import java.util.Collections;
15 import java.util.Dictionary;
16 import java.util.Enumeration;
17 import java.util.HashSet;
18 import java.util.Hashtable;
19 import java.util.List;
20 import org.apache.aries.blueprint.NamespaceHandler;
21 import org.apache.aries.blueprint.services.BlueprintExtenderService;
22 import org.apache.aries.quiesce.participant.QuiesceParticipant;
23 import org.apache.aries.util.AriesFrameworkUtil;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.gaul.modernizer_maven_annotations.SuppressModernizer;
26 import org.opendaylight.controller.blueprint.ext.OpendaylightNamespaceHandler;
27 import org.opendaylight.yangtools.util.xml.UntrustedXML;
28 import org.osgi.framework.Bundle;
29 import org.osgi.framework.BundleActivator;
30 import org.osgi.framework.BundleContext;
31 import org.osgi.framework.BundleEvent;
32 import org.osgi.framework.ServiceReference;
33 import org.osgi.framework.ServiceRegistration;
34 import org.osgi.framework.SynchronousBundleListener;
35 import org.osgi.service.blueprint.container.BlueprintContainer;
36 import org.osgi.service.blueprint.container.BlueprintEvent;
37 import org.osgi.service.blueprint.container.BlueprintListener;
38 import org.osgi.util.tracker.BundleTracker;
39 import org.osgi.util.tracker.BundleTrackerCustomizer;
40 import org.osgi.util.tracker.ServiceTracker;
41 import org.osgi.util.tracker.ServiceTrackerCustomizer;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
44
45 /**
46  * This class is created in bundle activation and scans ACTIVE bundles for blueprint XML files located under
47  * the well-known org/opendaylight/blueprint/ path and deploys the XML files via the Aries
48  * BlueprintExtenderService. This path differs from the standard OSGI-INF/blueprint path to allow for
49  * controlled deployment of blueprint containers in an orderly manner.
50  *
51  * @author Thomas Pantelis
52  */
53 public class BlueprintBundleTracker implements BundleActivator, BundleTrackerCustomizer<Bundle>, BlueprintListener,
54         SynchronousBundleListener {
55     private static final Logger LOG = LoggerFactory.getLogger(BlueprintBundleTracker.class);
56     private static final String ODL_CUSTOM_BLUEPRINT_FILE_PATH = "org/opendaylight/blueprint/";
57     private static final String STANDARD_BLUEPRINT_FILE_PATH = "OSGI-INF/blueprint/";
58     private static final String BLUEPRINT_FLE_PATTERN = "*.xml";
59     private static final long SYSTEM_BUNDLE_ID = 0;
60
61     private ServiceTracker<BlueprintExtenderService, BlueprintExtenderService> blueprintExtenderServiceTracker;
62     private ServiceTracker<QuiesceParticipant, QuiesceParticipant> quiesceParticipantTracker;
63     private BundleTracker<Bundle> bundleTracker;
64     private BundleContext bundleContext;
65     private volatile BlueprintExtenderService blueprintExtenderService;
66     private volatile QuiesceParticipant quiesceParticipant;
67     private volatile ServiceRegistration<?> blueprintContainerRestartReg;
68     private volatile BlueprintContainerRestartServiceImpl restartService;
69     private volatile boolean shuttingDown;
70     private ServiceRegistration<?> eventHandlerReg;
71     private ServiceRegistration<?> namespaceReg;
72
73     /**
74      * Implemented from BundleActivator.
75      */
76     @Override
77     public void start(final BundleContext context) {
78         LOG.info("Starting {}", getClass().getSimpleName());
79
80         // CONTROLLER-1867: force UntrustedXML initialization, so that it uses our TCCL to initialize
81         UntrustedXML.newDocumentBuilder();
82
83         restartService = new BlueprintContainerRestartServiceImpl();
84
85         bundleContext = context;
86
87         registerBlueprintEventHandler(context);
88
89         registerNamespaceHandler(context);
90
91         bundleTracker = new BundleTracker<>(context, Bundle.ACTIVE, this);
92
93         blueprintExtenderServiceTracker = new ServiceTracker<>(context, BlueprintExtenderService.class,
94                 new ServiceTrackerCustomizer<BlueprintExtenderService, BlueprintExtenderService>() {
95                     @Override
96                     public BlueprintExtenderService addingService(
97                             final ServiceReference<BlueprintExtenderService> reference) {
98                         return onBlueprintExtenderServiceAdded(reference);
99                     }
100
101                     @Override
102                     public void modifiedService(final ServiceReference<BlueprintExtenderService> reference,
103                             final BlueprintExtenderService service) {
104                     }
105
106                     @Override
107                     public void removedService(final ServiceReference<BlueprintExtenderService> reference,
108                             final BlueprintExtenderService service) {
109                     }
110                 });
111         blueprintExtenderServiceTracker.open();
112
113         quiesceParticipantTracker = new ServiceTracker<>(context, QuiesceParticipant.class,
114                 new ServiceTrackerCustomizer<QuiesceParticipant, QuiesceParticipant>() {
115                     @Override
116                     public QuiesceParticipant addingService(
117                             final ServiceReference<QuiesceParticipant> reference) {
118                         return onQuiesceParticipantAdded(reference);
119                     }
120
121                     @Override
122                     public void modifiedService(final ServiceReference<QuiesceParticipant> reference,
123                                                 final QuiesceParticipant service) {
124                     }
125
126                     @Override
127                     public void removedService(final ServiceReference<QuiesceParticipant> reference,
128                                                final QuiesceParticipant service) {
129                     }
130                 });
131         quiesceParticipantTracker.open();
132     }
133
134     @SuppressFBWarnings(value = "UPM_UNCALLED_PRIVATE_METHOD",
135             justification = "https://github.com/spotbugs/spotbugs/issues/811")
136     private QuiesceParticipant onQuiesceParticipantAdded(final ServiceReference<QuiesceParticipant> reference) {
137         quiesceParticipant = reference.getBundle().getBundleContext().getService(reference);
138
139         LOG.debug("Got QuiesceParticipant");
140
141         restartService.setQuiesceParticipant(quiesceParticipant);
142
143         return quiesceParticipant;
144     }
145
146     @SuppressFBWarnings(value = "UPM_UNCALLED_PRIVATE_METHOD",
147             justification = "https://github.com/spotbugs/spotbugs/issues/811")
148     private BlueprintExtenderService onBlueprintExtenderServiceAdded(
149             final ServiceReference<BlueprintExtenderService> reference) {
150         blueprintExtenderService = reference.getBundle().getBundleContext().getService(reference);
151         bundleTracker.open();
152
153         bundleContext.addBundleListener(BlueprintBundleTracker.this);
154
155         LOG.debug("Got BlueprintExtenderService");
156
157         restartService.setBlueprintExtenderService(blueprintExtenderService);
158
159         blueprintContainerRestartReg = bundleContext.registerService(BlueprintContainerRestartService.class,
160             restartService, null);
161
162         return blueprintExtenderService;
163     }
164
165     private void registerNamespaceHandler(final BundleContext context) {
166         Dictionary<String, Object> props = emptyDict();
167         props.put("osgi.service.blueprint.namespace", OpendaylightNamespaceHandler.NAMESPACE_1_0_0);
168         namespaceReg = context.registerService(NamespaceHandler.class, new OpendaylightNamespaceHandler(), props);
169     }
170
171     private void registerBlueprintEventHandler(final BundleContext context) {
172         eventHandlerReg = context.registerService(BlueprintListener.class, this, null);
173     }
174
175     @SuppressModernizer
176     private static Dictionary<String, Object> emptyDict() {
177         return new Hashtable<>();
178     }
179
180     /**
181      * Implemented from BundleActivator.
182      */
183     @Override
184     public void stop(final BundleContext context) {
185         bundleTracker.close();
186         blueprintExtenderServiceTracker.close();
187         quiesceParticipantTracker.close();
188
189         AriesFrameworkUtil.safeUnregisterService(eventHandlerReg);
190         AriesFrameworkUtil.safeUnregisterService(namespaceReg);
191         AriesFrameworkUtil.safeUnregisterService(blueprintContainerRestartReg);
192     }
193
194     /**
195      * Implemented from SynchronousBundleListener.
196      */
197     @Override
198     public void bundleChanged(final BundleEvent event) {
199         // If the system bundle (id 0) is stopping, do an orderly shutdown of all blueprint containers. On
200         // shutdown the system bundle is stopped first.
201         if (event.getBundle().getBundleId() == SYSTEM_BUNDLE_ID && event.getType() == BundleEvent.STOPPING) {
202             shutdownAllContainers();
203         }
204     }
205
206     /**
207      * Implemented from BundleActivator.
208      */
209     @Override
210     public Bundle addingBundle(final Bundle bundle, final BundleEvent event) {
211         modifiedBundle(bundle, event, bundle);
212         return bundle;
213     }
214
215     /**
216      * Implemented from BundleTrackerCustomizer.
217      */
218     @Override
219     public void modifiedBundle(final Bundle bundle, final BundleEvent event, final Bundle object) {
220         if (shuttingDown) {
221             return;
222         }
223
224         if (bundle.getState() == Bundle.ACTIVE) {
225             List<Object> paths = findBlueprintPaths(bundle, ODL_CUSTOM_BLUEPRINT_FILE_PATH);
226
227             if (!paths.isEmpty()) {
228                 LOG.info("Creating blueprint container for bundle {} with paths {}", bundle, paths);
229
230                 blueprintExtenderService.createContainer(bundle, paths);
231             }
232         }
233     }
234
235     /**
236      * Implemented from BundleTrackerCustomizer.
237      */
238     @Override
239     public void removedBundle(final Bundle bundle, final BundleEvent event, final Bundle object) {
240         // BlueprintExtenderService will handle this.
241     }
242
243     /**
244      * Implemented from BlueprintListener to listen for blueprint events.
245      *
246      * @param event the event to handle
247      */
248     @Override
249     public void blueprintEvent(final BlueprintEvent event) {
250         if (event.getType() == BlueprintEvent.CREATED) {
251             LOG.info("Blueprint container for bundle {} was successfully created", event.getBundle());
252             return;
253         }
254
255         // If the container timed out waiting for dependencies, we'll destroy it and start it again. This
256         // is indicated via a non-null DEPENDENCIES property containing the missing dependencies. The
257         // default timeout is 5 min and ideally we would set this to infinite but the timeout can only
258         // be set at the bundle level in the manifest - there's no way to set it globally.
259         if (event.getType() == BlueprintEvent.FAILURE && event.getDependencies() != null) {
260             Bundle bundle = event.getBundle();
261
262             List<Object> paths = findBlueprintPaths(bundle);
263             if (!paths.isEmpty()) {
264                 LOG.warn("Blueprint container for bundle {} timed out waiting for dependencies - restarting it",
265                         bundle);
266
267                 restartService.restartContainer(bundle, paths);
268             }
269         }
270     }
271
272     static List<Object> findBlueprintPaths(final Bundle bundle) {
273         List<Object> paths = findBlueprintPaths(bundle, STANDARD_BLUEPRINT_FILE_PATH);
274         return !paths.isEmpty() ? paths : findBlueprintPaths(bundle, ODL_CUSTOM_BLUEPRINT_FILE_PATH);
275     }
276
277     @SuppressWarnings({ "rawtypes", "unchecked" })
278     private static List<Object> findBlueprintPaths(final Bundle bundle, final String path) {
279         Enumeration<?> rntries = bundle.findEntries(path, BLUEPRINT_FLE_PATTERN, false);
280         if (rntries == null) {
281             return Collections.emptyList();
282         } else {
283             return Collections.list((Enumeration)rntries);
284         }
285     }
286
287     private void shutdownAllContainers() {
288         shuttingDown = true;
289
290         restartService.close();
291
292         LOG.info("Shutting down all blueprint containers...");
293
294         Collection<Bundle> containerBundles = new HashSet<>(Arrays.asList(bundleContext.getBundles()));
295         while (!containerBundles.isEmpty()) {
296             // For each iteration of getBundlesToDestroy, as containers are destroyed, other containers become
297             // eligible to be destroyed. We loop until we've destroyed them all.
298             for (Bundle bundle : getBundlesToDestroy(containerBundles)) {
299                 containerBundles.remove(bundle);
300                 BlueprintContainer container = blueprintExtenderService.getContainer(bundle);
301                 if (container != null) {
302                     blueprintExtenderService.destroyContainer(bundle, container);
303                 }
304             }
305         }
306
307         LOG.info("Shutdown of blueprint containers complete");
308     }
309
310     private static List<Bundle> getBundlesToDestroy(final Collection<Bundle> containerBundles) {
311         List<Bundle> bundlesToDestroy = new ArrayList<>();
312
313         // Find all container bundles that either have no registered services or whose services are no
314         // longer in use.
315         for (Bundle bundle : containerBundles) {
316             ServiceReference<?>[] references = bundle.getRegisteredServices();
317             int usage = 0;
318             if (references != null) {
319                 for (ServiceReference<?> reference : references) {
320                     usage += getServiceUsage(reference);
321                 }
322             }
323
324             LOG.debug("Usage for bundle {} is {}", bundle, usage);
325             if (usage == 0) {
326                 bundlesToDestroy.add(bundle);
327             }
328         }
329
330         if (!bundlesToDestroy.isEmpty()) {
331             bundlesToDestroy.sort((b1, b2) -> (int) (b2.getLastModified() - b1.getLastModified()));
332
333             LOG.debug("Selected bundles {} for destroy (no services in use)", bundlesToDestroy);
334         } else {
335             // There's either no more container bundles or they all have services being used. For
336             // the latter it means there's either circular service usage or a service is being used
337             // by a non-container bundle. But we need to make progress so we pick the bundle with a
338             // used service with the highest service ID. Each service is assigned a monotonically
339             // increasing ID as they are registered. By picking the bundle with the highest service
340             // ID, we're picking the bundle that was (likely) started after all the others and thus
341             // is likely the safest to destroy at this point.
342
343             Bundle bundle = findBundleWithHighestUsedServiceId(containerBundles);
344             if (bundle != null) {
345                 bundlesToDestroy.add(bundle);
346             }
347
348             LOG.debug("Selected bundle {} for destroy (lowest ranking service or highest service ID)",
349                     bundlesToDestroy);
350         }
351
352         return bundlesToDestroy;
353     }
354
355     private static @Nullable Bundle findBundleWithHighestUsedServiceId(final Collection<Bundle> containerBundles) {
356         ServiceReference<?> highestServiceRef = null;
357         for (Bundle bundle : containerBundles) {
358             ServiceReference<?>[] references = bundle.getRegisteredServices();
359             if (references == null) {
360                 continue;
361             }
362
363             for (ServiceReference<?> reference : references) {
364                 // We did check the service usage previously but it's possible the usage has changed since then.
365                 if (getServiceUsage(reference) == 0) {
366                     continue;
367                 }
368
369                 // Choose 'reference' if it has a lower service ranking or, if the rankings are equal
370                 // which is usually the case, if it has a higher service ID. For the latter the < 0
371                 // check looks backwards but that's how ServiceReference#compareTo is documented to work.
372                 if (highestServiceRef == null || reference.compareTo(highestServiceRef) < 0) {
373                     LOG.debug("Currently selecting bundle {} for destroy (with reference {})", bundle, reference);
374                     highestServiceRef = reference;
375                 }
376             }
377         }
378
379         return highestServiceRef == null ? null : highestServiceRef.getBundle();
380     }
381
382     private static int getServiceUsage(final ServiceReference<?> ref) {
383         Bundle[] usingBundles = ref.getUsingBundles();
384         return usingBundles != null ? usingBundles.length : 0;
385     }
386 }