Migration of OpenDaylight based applications into lighty.io is really straightforward. But sometimes, you have to do more steps to run your application in lighty.io. Especially, if your application is using OSGi/Karaf specific code.
For example when it’s using BundleContext to get registered OSGi services in runtime (what is bad practice in OpenDaylight anyway).
This is the case in the ONAP SDN-C project. In this article, we will focus on this kind of problems and how we solved them.
BundleContext usage in SDN-C
In SDN-C BundleContext is mainly used for two things:
- Register implementation of some interface (service) to OSGi registry
- Get an instance of service from the OSGi registry
Firstly we are going to show how we dealt with code which registers service through OSGi and then what we did with the second case – when BundleContext is used to get services from the registry.
What we did with the code registering a service directly
Here is a sample code of registering service from SDNC MdsalResourceActivator class:
@Override public void start(BundleContext ctx) throws Exception { ... SvcLogicResource impl = new ConfigResource(...); LOG.debug("Registering MdsalResource service" +impl.getClass().getName()); registrations.add(ctx.registerService(impl.getClass().getName(), impl, null)); ... }
In this case, it is very simple. MdsalResourceActivator implements BundleActivator interface, which means it is initialized automatically by OSGI and after that, its start method is called. This will not happen in lighty.io, because it is not running in OSGi. What we had to do is just initialize the SvcLogicResource instance ourselves and store it for later use.
Here is a sample code from our factory implementation:
public class MdsalResourcesFactory { ... public static List<SvcLogicResource> createMdsalResources(...) { ... List<SvcLogicResource> logicResources = new ArrayList<>(2); SvcLogicResource logicResource = new ConfigResource(...); LOG.debug(...); logicResources.add(logicResource); logicResource = new OperationalResource(...); LOG.debug(...); logicResources.add(logicResource); return logicResources; } }
and here is a sample code of its usage:
public class SliAdaptorsModule extends AbstractLightyModule { ... private List<SvcLogicResource> mdsalResources; private List<SvcLogicResource> logicResources; ... @Override protected boolean initProcedure() { ... this.mdsalResources = MdsalResourcesFactory.createMdsalResources(...); if (this.mdsalResources.isEmpty()) { LOG.error("MDSAL logic resources are not present."); return false; } this.logicResources = new ArrayList<>(); ... this.logicResources.addAll(this.mdsalResources); return true; } ... public synchronized Optional<List<SvcLogicResource>> getSvcLogicResources() { if (this.logicResources == null) { return Optional.empty(); } return Optional.of( Collections.unmodifiableList(this.logicResources)); } }
Please note that SliAdaptorsModule class extends AbstractLightyModule class. That means it is a LightyModule which you can start and stop. It’s one of many LightyModules created during SDNC migration.
Also, note the getSvcLogicResources() method. Through this method, we can access a list of all initialized SvcLogicResource implementations (not only ConfigResource and OperationalResource but also AAIService and SqlResource).
How to get a direct service in your code
Now let’s take a look at what we did with the second case of BundleContext usage in SDNC. To be consistent with the previous example, here is a code sample from the SDN-C SvcLogicNodeExecutor class, which uses BundleContext to get SvcLogicResource service (implementation):
protected SvcLogicResource getSvcLogicResource(String plugin) { BundleContext bctx = FrameworkUtil.getBundle(this.getClass()) .getBundleContext(); ServiceReference sref = bctx.getServiceReference(plugin); if (sref != null) { SvcLogicResource resourcePlugin = (SvcLogicResource) bctx .getService(sref); return resourcePlugin; } ... }
Probably the best way how to remove this code is to inject the required service through a constructor or setter. In our case, we have copied the whole SvcLogicNodeExecutor class and changed it in a way that we can inject a list of SvcLogicResource through the setter method. Here is a sample code of our implementation:
public abstract class SvcLogicNodeExecutor { private final Map<String, SvcLogicResource> logicResources; ... public SvcLogicNodeExecutor() { this.logicResources = new HashMap<>(); ... } ... protected SvcLogicResource getSvcLogicResource(String plugin) { return this.logicResources.get(plugin); } void setLogicResources(Map<String, SvcLogicResource> logicResources) { this.logicResources.putAll(logicResources); } ... }
We changed the implementation of the getSvcLogicResource method, so it now does not do any lookup in the service registry through BundleContext.
One thing to note is that in the previous example, we stored SvcLogicResources as a list, but here it’s a map. The reason being the keys of this map (thus plugin parameter in getSvcLogicResource method) are just class names of its values (getClass().getName()).
Here is a sample code of how we set SvcLogicResources to our SvcLogicNodeExecutor:
private static Map<String, SvcLogicNodeExecutor> createBuiltinExecutors(..., Map<String, SvcLogicResource> logicResources) { Map<String, SvcLogicNodeExecutor> builtinExecutors = new HashMap<>(); builtinExecutors.put("block", new BlockNodeExecutor()); ... builtinExecutors.put("break", new BreakNodeExecutor()); builtinExecutors.values().forEach(executor -> { ... executor.setLogicResources(logicResources); }); return builtinExecutors; }
In this case, we used injection through the setter method, which may not be the best solution in many cases. We will explain why we didn’t use injection through constructor later.
The “Cascade copy” problem
In an ideal world, where dependency injection is used properly and code is decomposed in the right way (of course it’s not always possible, that’s why it is ideal), migrating to lighty.io is just a manner of instantiation of classes. (beans if you want).
These are originally instantiated by some dependency injection systems (Blueprint for example). But we don’t live in an ideal world, so things are often more complicated (it would not be fun either way).
One example of such complication is something that we call the “Cascade copy” problem.
The theory is very simple. Let’s say we have class A which is initialized by your dependency injection system. Class A depends on (uses/extend/etc.) class B and it depends on class C.
Now in class C, you have some code, which in runtime tries to get some service (for example via BundleContext, to be consistent). Problem is, that if you copy class C and change its implementation to get rid of BundleContext you will realize, that you have to also copy class B because you changed the package of class C.
And then you have to copy class A even if there will not be other changes, just changes in imports. Some sort of this is also present in SDNC.
In the previous example, we showed you a snippet from SvcLogicNodeExecutor class. We had to change its original implementation to get rid of BundleContext. However, this class is abstract and has 19 implementations. Therefore, we had to copy all these 19 classes, and change the import of SvcLogicNodeExecutor to point to our implementation.
Now you see, why we used a SvcLogicNodeExecutor injection through the setter method. It’s simply because we want to avoid too many changes in its 19 implementations.
SvcLogicNodeExecutor implementation was created and registered in SvcLogicActivator class which implements BundleActivator, so we had to rewrite initialization. Here is a relevant snippet from the SDN-C SvcLogicActivator class:
public class SvcLogicActivator implements BundleActivator { ... private static final Map<String, SvcLogicNodeExecutor> BUILTIN_NODES = new HashMap<String, SvcLogicNodeExecutor>() { { put("block", new BlockNodeExecutor()); put("call", new CallNodeExecutor()); ... other executors here ... put("update", new UpdateNodeExecutor()); put("break", new BreakNodeExecutor()); } }; ... @Override public void start(BundleContext ctx) throws Exception { ... try { SvcLogicStore store = getStore(); registerNodeTypes(store); } catch (ConfigurationException e) { LOG.warn("Could not initialize SvcLogicScore", e); } ... } ... private static void registerNodeTypes(SvcLogicStore store) throws SvcLogicException { ... Hashtable propTable = new Hashtable(); for (String nodeType : BUILTIN_NODES.keySet()) { LOG.info("SLI - registering node type {}", nodeType); propTable.clear(); propTable.put("nodeType", nodeType); ServiceRegistration reg = bundleCtx.registerService( SvcLogicNodeExecutor.class.getName(), BUILTIN_NODES.get(nodeType), propTable); ... } } } Example of our implementation of executors initialization is here: public class SvcLogicServiceImpl implements SvcLogicService { private final Map<String, SvcLogicNodeExecutor> nodeExecutors; public SvcLogicServiceImpl(...) throws SvcLogicException { ... this.nodeExecutors = createBuiltinExecutors(storeFactory, logicJavaPlugins, logicRecorders, logicResources); } ... private static Map<String, SvcLogicNodeExecutor> createBuiltinExecutors(... ) { Map<String, SvcLogicNodeExecutor> builtinExecutors = new HashMap<>(); builtinExecutors.put("block", new BlockNodeExecutor()); builtinExecutors.put("call", new CallNodeExecutor()); ... other executors here ... builtinExecutors.put("update", new UpdateNodeExecutor()); builtinExecutors.put("break", new BreakNodeExecutor()); ... } ... }
The problem summary is simple; we had to just copy all dependent classes and change their imports.
In practice, it required a little bit more investigation, because we first had to recursively find all usages of class to the top of “dependency tree”, where the top is some class which is not used directly anywhere (or is hidden behind an implemented interface, or it’s initialized by dependency injection system and is not injected anywhere).
This should be easy if you know your codebase you are trying to migrate.
Summary
In this article, we talked about how we dealt with some problems we encountered in the migration of ONAP SDN-C to lighty.io. There are many more problems in SDNC we didn’t mention. For example, we found:
- 4 different dependency injection systems used through the project – Blueprint, old ODL ConfigSubsystem, SpringDM, direct use of OSGi service registry
- Property files are loaded from the file system in many places instead of configuration injection.
We also didn’t show you our main class, which integrates everything together (but you may see it in our video).
All of this may be a topic for our next articles.
Andrej Záň
ONAP SDN-C repositories:
https://gerrit.onap.org/r/#/admin/projects/ccsdk/parent
https://gerrit.onap.org/r/#/admin/projects/ccsdk/sli/adaptors
https://gerrit.onap.org/r/#/admin/projects/ccsdk/sli/core
https://gerrit.onap.org/r/#/admin/projects/ccsdk/sli/northbound
https://gerrit.onap.org/r/#/admin/projects/ccsdk/sli/plugins
https://gerrit.onap.org/r/#/admin/projects/ccsdk/distribution