Add "specific-reference-list" blueprint extension 67/40267/4
authorTom Pantelis <tpanteli@brocade.com>
Mon, 13 Jun 2016 22:37:58 +0000 (18:37 -0400)
committerTom Pantelis <tpanteli@brocade.com>
Wed, 22 Jun 2016 13:44:02 +0000 (13:44 +0000)
Added a blueprint extension, "specific-reference-list", that obtains a specific
list of service instances from the OSGi registry for a given interface. The
specific list is learned by first extracting the list of expected service types
by inspecting RESOLVED bundles for a resource file under META-INF/services with
the same name as the given interface. The type(s) listed in the resource file
must match the "type" property of the advertised service(s). In this manner, an
app bundle announces the service type(s) that it will advertise so that the
extension knows which services to expect up front. Once all the expected services
are obtained, the container is notified that all dependencies are satisfied.

This new extension will initially be used by the bgpcep project.

Change-Id: I3bc6a72134b33c744fbb48fd645dd3a0ca54673d
Signed-off-by: Tom Pantelis <tpanteli@brocade.com>
opendaylight/blueprint/src/main/java/org/opendaylight/controller/blueprint/ext/AbstractDependentComponentFactoryMetadata.java
opendaylight/blueprint/src/main/java/org/opendaylight/controller/blueprint/ext/DataStoreAppConfigMetadata.java
opendaylight/blueprint/src/main/java/org/opendaylight/controller/blueprint/ext/OpendaylightNamespaceHandler.java
opendaylight/blueprint/src/main/java/org/opendaylight/controller/blueprint/ext/RpcServiceMetadata.java
opendaylight/blueprint/src/main/java/org/opendaylight/controller/blueprint/ext/SpecificReferenceListMetadata.java [new file with mode: 0644]
opendaylight/blueprint/src/main/resources/opendaylight-blueprint-ext-1.0.0.xsd

index bdcc1ee..a84b8b6 100644 (file)
@@ -13,11 +13,14 @@ import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Consumer;
+import javax.annotation.Nullable;
 import org.apache.aries.blueprint.di.AbstractRecipe;
 import org.apache.aries.blueprint.di.ExecutionContext;
 import org.apache.aries.blueprint.di.Recipe;
 import org.apache.aries.blueprint.ext.DependentComponentFactoryMetadata;
 import org.apache.aries.blueprint.services.ExtendedBlueprintContainer;
+import org.opendaylight.controller.blueprint.BlueprintContainerRestartService;
+import org.osgi.framework.ServiceReference;
 import org.osgi.service.blueprint.container.ComponentDefinitionException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -30,10 +33,12 @@ abstract class AbstractDependentComponentFactoryMetadata implements DependentCom
     private final String id;
     private final AtomicBoolean started = new AtomicBoolean();
     private final AtomicBoolean satisfied = new AtomicBoolean();
+    private final AtomicBoolean restarting = new AtomicBoolean();
     private final List<StaticServiceReferenceRecipe> serviceRecipes = new ArrayList<>();
     private volatile ExtendedBlueprintContainer container;
     private volatile SatisfactionCallback satisfactionCallback;
     private volatile String failureMessage;
+    private volatile Throwable failureCause;
     private volatile String dependendencyDesc;
 
     protected AbstractDependentComponentFactoryMetadata(String id) {
@@ -68,7 +73,12 @@ abstract class AbstractDependentComponentFactoryMetadata implements DependentCom
     protected abstract void startTracking();
 
     protected void setFailureMessage(String failureMessage) {
+        setFailure(failureMessage, null);
+    }
+
+    protected void setFailure(String failureMessage, Throwable failureCause) {
         this.failureMessage = failureMessage;
+        this.failureCause = failureCause;
     }
 
     protected void setDependendencyDesc(String dependendencyDesc) {
@@ -80,8 +90,9 @@ abstract class AbstractDependentComponentFactoryMetadata implements DependentCom
     }
 
     protected void setSatisfied() {
-        satisfied.set(true);
-        satisfactionCallback.notifyChanged();
+        if(satisfied.compareAndSet(false, true)) {
+            satisfactionCallback.notifyChanged();
+        }
     }
 
     protected void retrieveService(String name, Class<?> interfaceClass, Consumer<Object> onServiceRetrieved) {
@@ -107,7 +118,7 @@ abstract class AbstractDependentComponentFactoryMetadata implements DependentCom
 
     protected void onCreate() throws ComponentDefinitionException {
         if(failureMessage != null) {
-            throw new ComponentDefinitionException(failureMessage);
+            throw new ComponentDefinitionException(failureMessage, failureCause);
         }
 
         // The following code is a bit odd so requires some explanation. A little background... If a bean
@@ -179,4 +190,41 @@ abstract class AbstractDependentComponentFactoryMetadata implements DependentCom
 
         serviceRecipes.clear();
     }
+
+    protected void restartContainer() {
+        if(restarting.compareAndSet(false, true)) {
+            BlueprintContainerRestartService restartService = getOSGiService(BlueprintContainerRestartService.class);
+            if(restartService != null) {
+                log.debug("{}: Restarting container", logName());
+                restartService.restartContainerAndDependents(container().getBundleContext().getBundle());
+            }
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    @Nullable
+    protected <T> T getOSGiService(Class<T> serviceInterface) {
+        try {
+            ServiceReference<T> serviceReference =
+                    container().getBundleContext().getServiceReference(serviceInterface);
+            if(serviceReference == null) {
+                log.warn("{}: {} reference not found", logName(), serviceInterface.getSimpleName());
+                return null;
+            }
+
+            T service = (T)container().getService(serviceReference);
+            if(service == null) {
+                // This could happen on shutdown if the service was already unregistered so we log as debug.
+                log.debug("{}: {} was not found", logName(), serviceInterface.getSimpleName());
+            }
+
+            return service;
+        } catch(IllegalStateException e) {
+            // This is thrown if the BundleContext is no longer valid which is possible on shutdown so we
+            // log as debug.
+            log.debug("{}: Error obtaining {}", logName(), serviceInterface.getSimpleName(), e);
+        }
+
+        return null;
+    }
 }
index c1f2842..f8a6127 100644 (file)
@@ -21,7 +21,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 import org.apache.aries.blueprint.services.ExtendedBlueprintContainer;
-import org.opendaylight.controller.blueprint.BlueprintContainerRestartService;
 import org.opendaylight.controller.md.sal.binding.api.ClusteredDataTreeChangeListener;
 import org.opendaylight.controller.md.sal.binding.api.DataBroker;
 import org.opendaylight.controller.md.sal.binding.api.DataObjectModification;
@@ -50,7 +49,6 @@ import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
 import org.opendaylight.yangtools.yang.model.api.Module;
 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
-import org.osgi.framework.ServiceReference;
 import org.osgi.service.blueprint.container.ComponentDefinitionException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -336,39 +334,6 @@ public class DataStoreAppConfigMetadata extends AbstractDependentComponentFactor
         return dataNode;
     }
 
-    private void restartContainer() {
-        BlueprintContainerRestartService restartService = getOSGiService(BlueprintContainerRestartService.class);
-        if(restartService != null) {
-            restartService.restartContainerAndDependents(container().getBundleContext().getBundle());
-        }
-    }
-
-    @SuppressWarnings("unchecked")
-    @Nullable
-    private <T> T getOSGiService(Class<T> serviceInterface) {
-        try {
-            ServiceReference<T> serviceReference =
-                    container().getBundleContext().getServiceReference(serviceInterface);
-            if(serviceReference == null) {
-                LOG.warn("{}: {} reference not found", logName(), serviceInterface.getSimpleName());
-                return null;
-            }
-
-            T service = (T)container().getService(serviceReference);
-            if(service == null) {
-                // This could happen on shutdown if the service was already unregistered so we log as debug.
-                LOG.debug("{}: {} was not found", logName(), serviceInterface.getSimpleName());
-            }
-
-            return service;
-        } catch(IllegalStateException e) {
-            // This is thrown if the BundleContext is no longer valid which is possible on shutdown so we
-            // log as debug.
-            LOG.debug("{}: Error obtaining {}", logName(), serviceInterface.getSimpleName(), e);
-        }
-
-        return null;
-    }
 
     @Override
     public void destroy(Object instance) {
index 7a1c86b..ab9e216 100644 (file)
@@ -53,17 +53,18 @@ public class OpendaylightNamespaceHandler implements NamespaceHandler {
     static final String ROUTED_RPC_REG_CONVERTER_NAME = "org.opendaylight.blueprint.RoutedRpcRegConverter";
     static final String RPC_REGISTRY_NAME = "org.opendaylight.blueprint.RpcRegistry";
     static final String NOTIFICATION_SERVICE_NAME = "org.opendaylight.blueprint.NotificationService";
+    static final String TYPE_ATTR = "type";
 
     private static final Logger LOG = LoggerFactory.getLogger(OpendaylightNamespaceHandler.class);
     private static final String COMPONENT_PROCESSOR_NAME = ComponentProcessor.class.getName();
     private static final String RESTART_DEPENDENTS_ON_UPDATES = "restart-dependents-on-updates";
     private static final String USE_DEFAULT_FOR_REFERENCE_TYPES = "use-default-for-reference-types";
     private static final String CLUSTERED_APP_CONFIG = "clustered-app-config";
-    private static final String TYPE_ATTR = "type";
     private static final String INTERFACE = "interface";
     private static final String REF_ATTR = "ref";
     private static final String ID_ATTR = "id";
     private static final String RPC_SERVICE = "rpc-service";
+    private static final String SPECIFIC_SERVICE_REF_LIST = "specific-reference-list";
 
     @SuppressWarnings("rawtypes")
     @Override
@@ -96,6 +97,8 @@ public class OpendaylightNamespaceHandler implements NamespaceHandler {
             return parseNotificationListener(element, context);
         } else if (nodeNameEquals(element, CLUSTERED_APP_CONFIG)) {
             return parseClusteredAppConfig(element, context);
+        } else if (nodeNameEquals(element, SPECIFIC_SERVICE_REF_LIST)) {
+            return parseSpecificReferenceList(element, context);
         }
 
         throw new ComponentDefinitionException("Unsupported standalone element: " + element.getNodeName());
@@ -346,6 +349,15 @@ public class OpendaylightNamespaceHandler implements NamespaceHandler {
                         DataStoreAppConfigMetadata.LIST_KEY_VALUE), defaultAppConfigElement);
     }
 
+    private Metadata parseSpecificReferenceList(Element element, ParserContext context) {
+        ComponentFactoryMetadata metadata = new SpecificReferenceListMetadata(getId(context, element),
+                element.getAttribute(INTERFACE));
+
+        LOG.debug("parseSpecificReferenceList returning {}", metadata);
+
+        return metadata;
+    }
+
     private Element parseXML(String name, String xml) {
         DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
         builderFactory.setNamespaceAware(true);
index e2260a6..82b1b6b 100644 (file)
@@ -130,6 +130,8 @@ class RpcServiceMetadata extends AbstractDependentComponentFactoryMetadata {
     public Object create() throws ComponentDefinitionException {
         LOG.debug("{}: In create: interfaceName: {}", logName(), interfaceName);
 
+        super.onCreate();
+
         try {
             RpcService rpcService = rpcRegistry.getRpcService(rpcInterface);
 
diff --git a/opendaylight/blueprint/src/main/java/org/opendaylight/controller/blueprint/ext/SpecificReferenceListMetadata.java b/opendaylight/blueprint/src/main/java/org/opendaylight/controller/blueprint/ext/SpecificReferenceListMetadata.java
new file mode 100644 (file)
index 0000000..1aef141
--- /dev/null
@@ -0,0 +1,220 @@
+/*
+ * Copyright (c) 2016 Brocade Communications Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.controller.blueprint.ext;
+
+import com.google.common.base.Charsets;
+import com.google.common.collect.ImmutableList;
+import com.google.common.io.Resources;
+import java.io.IOException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.ConcurrentSkipListSet;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleEvent;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.blueprint.container.ComponentDefinitionException;
+import org.osgi.util.tracker.BundleTracker;
+import org.osgi.util.tracker.BundleTrackerCustomizer;
+import org.osgi.util.tracker.ServiceTracker;
+import org.osgi.util.tracker.ServiceTrackerCustomizer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Factory metadata corresponding to the "specific-reference-list" element that obtains a specific list
+ * of service instances from the OSGi registry for a given interface. The specific list is learned by first
+ * extracting the list of expected service types by inspecting RESOLVED bundles for a resource file under
+ * META-INF/services with the same name as the given interface. The type(s) listed in the resource file
+ * must match the "type" property of the advertised service(s). In this manner, an app bundle announces the
+ * service type(s) that it will advertise so that this class knows which services to expect up front. Once
+ * all the expected services are obtained, the container is notified that all dependencies of this component
+ * factory are satisfied.
+ *
+ * @author Thomas Pantelis
+ */
+class SpecificReferenceListMetadata extends AbstractDependentComponentFactoryMetadata {
+    private static final Logger LOG = LoggerFactory.getLogger(SpecificReferenceListMetadata.class);
+
+    private final String interfaceName;
+    private final String serviceResourcePath;
+    private final Collection<String> expectedServiceTypes = new ConcurrentSkipListSet<>();
+    private final Collection<String> retrievedServiceTypes = new ConcurrentSkipListSet<>();
+    private final Collection<Object> retrievedServices = Collections.synchronizedList(new ArrayList<>());
+    private volatile BundleTracker<Bundle> bundleTracker;
+    private volatile ServiceTracker<Object, Object> serviceTracker;
+
+    SpecificReferenceListMetadata(String id, String interfaceName) {
+        super(id);
+        this.interfaceName = interfaceName;
+        serviceResourcePath = "META-INF/services/" + interfaceName;
+    }
+
+    @Override
+    protected void startTracking() {
+        BundleTrackerCustomizer<Bundle> bundleListener = new BundleTrackerCustomizer<Bundle>() {
+            @Override
+            public Bundle addingBundle(Bundle bundle, BundleEvent event) {
+                bundleAdded(bundle);
+                return bundle;
+            }
+
+            @Override
+            public void modifiedBundle(Bundle bundle, BundleEvent event, Bundle object) {
+            }
+
+            @Override
+            public void removedBundle(Bundle bundle, BundleEvent event, Bundle object) {
+            }
+        };
+
+        bundleTracker = new BundleTracker<>(container().getBundleContext(), Bundle.RESOLVED | Bundle.STARTING |
+                Bundle.STOPPING | Bundle.ACTIVE, bundleListener);
+
+        // This will get the list of all current RESOLVED+ bundles.
+        bundleTracker.open();
+
+        if(expectedServiceTypes.isEmpty()) {
+            setSatisfied();
+            return;
+        }
+
+        ServiceTrackerCustomizer<Object, Object> serviceListener = new ServiceTrackerCustomizer<Object, Object>() {
+            @Override
+            public Object addingService(ServiceReference<Object> reference) {
+                return serviceAdded(reference);
+            }
+
+            @Override
+            public void modifiedService(ServiceReference<Object> reference, Object service) {
+            }
+
+            @Override
+            public void removedService(ServiceReference<Object> reference, Object service) {
+                container().getBundleContext().ungetService(reference);
+            }
+        };
+
+        setDependendencyDesc(interfaceName + " services with types " + expectedServiceTypes);
+
+        serviceTracker = new ServiceTracker<>(container().getBundleContext(), interfaceName, serviceListener);
+        serviceTracker.open();
+    }
+
+    private void bundleAdded(Bundle bundle) {
+        URL resource = bundle.getEntry(serviceResourcePath);
+        if(resource == null) {
+            return;
+        }
+
+        LOG.debug("{}: Found {} resource in bundle {}", logName(), resource, bundle.getSymbolicName());
+
+        try {
+            for(String line : Resources.readLines(resource, Charsets.UTF_8)) {
+                int ci = line.indexOf('#');
+                if(ci >= 0) {
+                    line = line.substring(0, ci);
+                }
+
+                line = line.trim();
+                if(line.isEmpty()) {
+                    continue;
+                }
+
+                String serviceType = line;
+                LOG.debug("{}: Retrieved service type {}", logName(), serviceType);
+                expectedServiceTypes.add(serviceType);
+            }
+        } catch(IOException e) {
+            setFailure(String.format("%s: Error reading resource %s from bundle %s", logName(), resource,
+                    bundle.getSymbolicName()), e);
+        }
+    }
+
+    private Object serviceAdded(ServiceReference<Object> reference) {
+        Object service = container().getBundleContext().getService(reference);
+        Object serviceType = reference.getProperty(OpendaylightNamespaceHandler.TYPE_ATTR);
+
+        LOG.debug("{}: Service type {} added from bundle {}", logName(), serviceType,
+                reference.getBundle().getSymbolicName());
+
+        if(serviceType == null) {
+            LOG.error("{}: Missing OSGi service property '{}' for service interface {} in bundle {}", logName(),
+                    OpendaylightNamespaceHandler.TYPE_ATTR, interfaceName,  reference.getBundle().getSymbolicName());
+            return service;
+        }
+
+        if(!expectedServiceTypes.contains(serviceType)) {
+            LOG.error("{}: OSGi service property '{}' for service interface {} in bundle {} was not found in the " +
+                    "expected service types {} obtained via {} bundle resources. Is the bundle resource missing or the service type misspelled?",
+                    logName(), OpendaylightNamespaceHandler.TYPE_ATTR, interfaceName, reference.getBundle().getSymbolicName(),
+                    expectedServiceTypes, serviceResourcePath);
+            return service;
+        }
+
+        // If already satisfied, meaning we got all initial services, then a new bundle must've been
+        // dynamically installed or a prior service's blueprint container was restarted, in which case we
+        // restart our container.
+        if(isSatisfied()) {
+            restartContainer();
+        } else {
+            retrievedServiceTypes.add(serviceType.toString());
+            retrievedServices.add(service);
+
+            if(retrievedServiceTypes.equals(expectedServiceTypes)) {
+                LOG.debug("{}: Got all expected service types", logName());
+                setSatisfied();
+            } else {
+                Set<String> remaining = new HashSet<>(expectedServiceTypes);
+                remaining.removeAll(retrievedServiceTypes);
+                setDependendencyDesc(interfaceName + " services with types " + remaining);
+            }
+        }
+
+        return service;
+    }
+
+    @Override
+    public Object create() throws ComponentDefinitionException {
+        LOG.debug("{}: In create: interfaceName: {}", logName(), interfaceName);
+
+        super.onCreate();
+
+        LOG.debug("{}: create returning service list {}", logName(), retrievedServices);
+
+        synchronized(retrievedServices) {
+            return ImmutableList.copyOf(retrievedServices);
+        }
+    }
+
+    @Override
+    public void destroy(Object instance) {
+        super.destroy(instance);
+
+        if(bundleTracker != null) {
+            bundleTracker.close();
+            bundleTracker = null;
+        }
+
+        if(serviceTracker != null) {
+            serviceTracker.close();
+            serviceTracker = null;
+        }
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("SpecificReferenceListMetadata [interfaceName=").append(interfaceName)
+                .append(", serviceResourcePath=").append(serviceResourcePath).append("]");
+        return builder.toString();
+    }
+}
index 6ae6395..2a2189d 100644 (file)
     <xsd:attribute name="id" type="xsd:ID" use="required"/>
   </xsd:complexType>
   <xsd:element name="clustered-app-config" type="TclusteredAppConfig"/>
+
+  <xsd:complexType name="TspecificReferenceList">
+    <xsd:attribute name="interface" type="bp:Tclass" use="required"/>
+    <xsd:attribute name="id" type="xsd:ID"/>
+  </xsd:complexType>
+  <xsd:element name="specific-reference-list" type="TspecificReferenceList"/>
 </xsd:schema>
\ No newline at end of file