I consider myself an eclectic person: I like a wide variety of things and love to try anything I haven’t before.
When applying it to entertainment, and specifically to films, I can watch sci-fi, drama, comedy, documentaries, … you name it.
I encourage everyone to be open-minded and give a try to that thing you think is weird and that someone else likes, that you do not understand why.
Today, I suggest you watch a classic 1930s movie named A Day At The Races, a comedy by the Marx Bros. Nothing compared to nowadays dramedies, but a small old jewel given to us almost a century ago.
And while you look for the movie, I am going to tell you how we faced a day at the Liferay race conditions!
The Plot
Exposition
The story begins with an awesome Portal (Liferay DXP 7.3/7.4), owned by a mistress in distress that has fallen on hard times. This Portal needed to comply with certain requirements that included overriding Liferay OSGI services.
The Bros appear at the scene to help the mistress! They place the best of their abilities to create custom services that would achieve what is needed. Everything works great locally, and they were happy ever after! End of story!
Conflict
End of story? NO!!! The awesome new functionality does not work in one of the higher environments! How can this be? It is working in every other environment, locally and remotely, but this one!!!!! Everything was fun and laughs and sunny days, but suddenly storm clouds loom on the horizon!
Either he’s dead or my watch has stopped.
Dr. Hackenbush
Rising Action
The Bros come back to the scene! They investigate the issue thoroughly! See if the module is deployed and active! It is!? Check the ranking! To the Gogo Shell! List the components in the system! Check their information! Everything seems fine? Add extra logs to your class! See when the module is activated! How can it be? It is just not working!! Check with Liferay Support! Do anything!! Now it works!! Now it doesn’t!! WHAT IS HAPPENING?!!!!
The Climax
HA! The GREEDY component reveals at last!! The Liferay OSGI service uses the matching, highest-ranking service as soon as it is available, which creates a race condition that may or may not have an effect on your environment! Sometimes it happens, sometimes it doesn’t! It is the greedy original component that is messing with the mistress in distress!
Falling (Into) Action
Alright! Now it is clear what we need to do: make sure that our overriding component takes effect, no matter what, at the right moment. For that, we are going to make sure our OSGI component, which is reusing the original component (so we only override what is needed), loads in the correct order.
Step 1: Create a utility class that is going to do the magic
This class will allow you to:
- Get a bundle from a bundle context.
- Get a component class from a bundle, using the service tracker.
- Get a component from a bundle, using the service component runtime.
- Enable a component.
- Disable a component.
Check this utility class:
package com.xtivia.osgi;
import com.liferay.osgi.service.tracker.collections.list.ServiceTrackerList;
import com.liferay.osgi.service.tracker.collections.list.ServiceTrackerListFactory;
import com.liferay.portal.kernel.log.Log;
import com.liferay.portal.kernel.log.LogFactoryUtil;
import java.util.Arrays;
import java.util.Iterator;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.runtime.ServiceComponentRuntime;
import org.osgi.service.component.runtime.dto.ComponentDescriptionDTO;
@Component(
immediate = true,
service = OsgiUtils.class
)
public class OsgiUtils {
private final Log log = LogFactoryUtil.getLog(OsgiUtils.class);
/**
* It returns a component class based on the bundle name, a class (or interface), and a component name
* It enables the class component if it is disabled
* @param componentBundleName
* @param clazz
* @param componentName
* @return
*/
public Object getComponentClass(BundleContext bundleContext, String componentBundleName, Class<?> clazz, String componentName) {
Bundle bundle = getBundle(bundleContext, componentBundleName);
enableComponent(bundle, componentName);
// Get component class
try {
return getComponentClass(bundle.getBundleContext(), clazz, componentName);
} catch (Exception e) {
log.error(e.toString(), e);
return null;
}
}
/**
* It returns a component class based on the bundle context, a class (or interface), and a component name
* It enables the class component if it is disabled
* If the component class is not found within the bundle context, it returns null
* @param otherBundleContext
* @param clazz
* @param componentName
* @return
*/
public Object getComponentClass(BundleContext otherBundleContext, Class<?> clazz, String componentName) {
if (log.isDebugEnabled()) {
log.debug("Ok, lets try getting the component class....");
}
ServiceTrackerList<?, ?> serviceTrackerList = ServiceTrackerListFactory.open(otherBundleContext, clazz);
if (log.isDebugEnabled()) {
log.debug("Total items in serviceTrackerList: "+serviceTrackerList.size());
}
Iterator<?> iter = serviceTrackerList.iterator();
while (iter.hasNext()) {
Object item = iter.next();
if (log.isDebugEnabled()) {
log.debug("Checking item: "+item.getClass().getCanonicalName());
}
if (item.getClass().getName().equals(componentName)) {
if (log.isDebugEnabled()) {
log.debug("Found the class! Returning "+item.getClass().getCanonicalName());
}
return item;
}
}
if (log.isWarnEnabled()) {
log.warn("Did not find a class for component="+componentName);
}
return null;
}
/**
* It disables a component if it exists within a bundle
* @param bundle
* @param componentName
*/
public void disableComponent(Bundle bundle, String componentName) {
ComponentDescriptionDTO component = getComponent(bundle, componentName);
if (component == null) {
if (log.isWarnEnabled()) {
log.warn("Could not disable component with name="+componentName+" in bundle with symbolic name="+bundle.getSymbolicName());
}
return;
}
if (log.isDebugEnabled()) {
log.debug("Disabling component with description="+component.toString());
}
if (serviceComponentRuntime.isComponentEnabled(component)) {
serviceComponentRuntime.disableComponent(component);
}
}
/**
* It enables a component if it exists within a bundle
* @param bundle
* @param componentName
*/
public void enableComponent(Bundle bundle, String componentName) {
ComponentDescriptionDTO component = getComponent(bundle, componentName);
if (component == null) {
if (log.isWarnEnabled()) {
log.warn("Could not enable component with name="+componentName+" in bundle with symbolic name="+bundle.getSymbolicName());
}
return;
}
if (!serviceComponentRuntime.isComponentEnabled(component)) {
serviceComponentRuntime.enableComponent(component);
}
}
/**
* Returns a component from a bundle based on its name, or null if none found
* @param bundle
* @param componentName
* @return
*/
public ComponentDescriptionDTO getComponent(Bundle bundle, String componentName) {
if (bundle == null) {
if (log.isWarnEnabled()) {
log.warn("Bundle is null. Returning a null component.");
}
return null;
}
if (log.isDebugEnabled()) {
log.debug("Retrieving component with name="+componentName);
}
ComponentDescriptionDTO component = serviceComponentRuntime.getComponentDescriptionDTO(bundle, componentName);
if (component == null) {
if (log.isWarnEnabled()) {
log.warn("Could not find find a component with name="+componentName+" in bundle with symbolic name="+bundle.getSymbolicName());
}
}
return component;
}
/**
* Returns a bundle based on its symbolic name, or null if none found
* @param bundleContext
* @param bundleSymbolicName
* @return
*/
public Bundle getBundle(BundleContext bundleContext, String bundleSymbolicName) {
Bundle[] bundles = bundleContext.getBundles();
Bundle bundle = Arrays.stream(bundles)
.filter(item -> bundleSymbolicName.equals(item.getSymbolicName()))
.findAny().orElse(null);
if (bundle == null) {
if (log.isWarnEnabled()) {
log.warn("Could not find find a bundle with symbolic name="+bundleSymbolicName);
}
}
if (log.isDebugEnabled()) {
log.debug("Retrieved bundle with symbolic name="+bundleSymbolicName);
}
return bundle;
}
@Reference
private ServiceComponentRuntime serviceComponentRuntime;
}
Disclaimer: There are small differences in this class between Liferay 7.3 and 7.4., i.e. ServiceTrackerList is an interface with two objects, while in 7.4 it only accepts one object. The utility shown above is for 7.3. Also this example has been converted to a service, while originally it was a utility class. Because why not.
Step 2: Use the utility class in your custom component
We are going to use an example with the SolrQueryAssembler, reimplementing the Solr query assembler from Liferay 7.3 to manipulate the query (Solr is not supported in 7.4, and only partially in 7.3). This is one of the cases where the race condition happened – but not the only one!
The class is self-explanatory, but in short:
- We reimplement a method, calling at some point the original implementation.
- When the class is activated, we disable the original component implementation.
- When we need to call the original implementation, the retrieve the original component and we enable it. We do this only once using a getter.
- When the class is deactivated, we make sure the original component implementation is enabled and ready to go. This is done to avoid a situation where our module is stopped (i.e. from the Gogo shell or because the module is removed) and there is no active component anymore, causing the system to crash.
package com.xtivia.osgi.tools.example;
import com.liferay.portal.search.engine.adapter.search.BaseSearchRequest;
import com.liferay.portal.search.solr8.internal.search.engine.adapter.search.BaseSolrQueryAssembler;
import com.liferay.portal.search.solr8.internal.search.engine.adapter.search.BaseSolrQueryAssemblerImpl;
import com.xtivia.osgi.tools.OsgiUtils;
import org.apache.solr.client.solrj.SolrQuery;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
@Component(
immediate = true,
property = {
"service.ranking:Integer=150"
},
service = BaseSolrQueryAssembler.class
)
public class OsgiUtilsExample implements BaseSolrQueryAssembler {
@Override
public void assemble(SolrQuery solrQuery, BaseSearchRequest baseSearchRequest) {
// Customize the code
doSomethingToCreateAnAwesomeQuery(solrQuery, baseSearchRequest);
// Call the original query
getBaseSolrQueryAssembler().assemble(solrQuery, baseSearchRequest);
}
private void doSomethingToCreateAnAwesomeQuery(SolrQuery solrQuery, BaseSearchRequest baseSearchRequest) {
// DO SOMETHING AWESOME!!!
}
/*
* NOTE TO THE DEVELOPER:
* In order to solve a race condition, we are forced to load the base query assembler in a different way.
* We do not use the Reference but load the component manually.
*
* Step 1: Disable original component when activating this bundle
* Step 2: Retrieve and enable the original component on demand to avoid the race
* Step 3: If this module is disabled live, we need to make sure the original component is enabled
*/
// @Reference(
// target = "(component.name=com.liferay.portal.search.solr8.internal.search.engine.adapter.search.BaseSolrQueryAssemblerImpl)"
// )
// private BaseSolrQueryAssembler baseSolrQueryAssembler;
// We define the original implementation class to reuse it later
private final Class<?> COMPONENT_CLASS = BaseSolrQueryAssembler.class;
private final String COMPONENT_NAME = BaseSolrQueryAssemblerImpl.class.getName();
private final String BUNDLE_NAME = "com.liferay.portal.search.solr8.impl";
@Reference
private OsgiUtils osgiUtils;
private BundleContext bundleContext;
/* Step 1: disable original component */
@Activate
public void activate(BundleContext bundleContext) {
// Save for later
this.bundleContext = bundleContext;
// We avoid the race condition disabling the original component.
osgiUtils.disableComponent(osgiUtils.getBundle(bundleContext, BUNDLE_NAME), COMPONENT_NAME);
}
/* Step 2: retrieve original component */
private volatile BaseSolrQueryAssembler baseSolrQueryAssembler;
private BaseSolrQueryAssembler getBaseSolrQueryAssembler() {
if (baseSolrQueryAssembler == null) {
// We retrieve the other bundle
Bundle otherBundle = osgiUtils.getBundle(bundleContext, BUNDLE_NAME);
// We retrieve the original class from the other bundle context
baseSolrQueryAssembler = (BaseSolrQueryAssembler) osgiUtils.getComponentClass(otherBundle.getBundleContext(), COMPONENT_CLASS, COMPONENT_NAME);
// If the original component is still disabled, we may need to enable it
osgiUtils.enableComponent(otherBundle, COMPONENT_NAME);
}
return baseSolrQueryAssembler;
}
/* Step 3: keep everything as it was */
@Deactivate
public void deactivate(BundleContext bundleContext) {
// Make sure the original component is enabled!
osgiUtils.enableComponent(osgiUtils.getBundle(bundleContext, BUNDLE_NAME), COMPONENT_NAME);
}
}
Disclaimer: These classes are similar to the original ones and untested!!
Resolution
The Bros came to rescue! They deployed the right solution (not these untested examples!), verified that everything was working fine, and then joined the mistress in distress and the rest of the folks, and altogether started singing joyfully while walking into the horizon!
If you’d like more information or have any questions, please contact us!