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