Waiting for Custom Lifecycle Events

How to handle waiting at startup for data to be available using a custom ModuleServiceLifecycle event...

Introduction

A community member recently posted a question about how to make their component wait. I of course pointed them at my Power of Patience blog which I have been using for years now to wait for the portal to be ready.

They came back and said it didn't work, which seemed quite odd to me because I and many others have been very successful with it.

After digging a little deeper though, it quickly became clear why it wasn't working. I came up with a cool solution and thought I'd share it here...

Why Waiting on Portal Ready Wasn't Enough

So effectively the community member wrote an UpgradeProcess implementation that was supposed to add resource permissions permissions to a role they created.

Now if they deployed this to an already running Liferay instance, all of this was fine.

However, if this was deployed prior to launching Liferay the first time, they got an exception:

ERROR [main][CustomUpgradeProcess:355] Upgrade process stopped because of 
    issue : com_liferay_roles_admin_web_portlet_RolesAdminPortlet#VIEW
    com.liferay.portal.kernel.exception.NoSuchResourceActionException: 
      com_liferay_roles_admin_web_portlet_RolesAdminPortlet#VIEW
  at com.liferay.portal.service.impl.ResourceActionLocalServiceImpl
    .getResourceAction(ResourceActionLocalServiceImpl.java:324)
  at jdk.internal.reflect.GeneratedMethodAccessor261.invoke(Unknown Source)
  at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl
    .invoke(DelegatingMethodAccessorImpl.java:43)
  at java.base/java.lang.reflect.Method.invoke(Method.java:566)
  at com.liferay.portal.spring.aop.AopMethodInvocationImpl
    .proceed(AopMethodInvocationImpl.java:41)
  at com.liferay.portal.spring.aop.AopInvocationHandler
    .invoke(AopInvocationHandler.java:40)
  at com.sun.proxy.$Proxy69.getResourceAction(Unknown Source)
  at com.liferay.portal.service.impl.ResourcePermissionLocalServiceImpl
    ._updateResourcePermission(ResourcePermissionLocalServiceImpl.java:2263)
  at com.liferay.portal.service.impl.ResourcePermissionLocalServiceImpl
    .updateResourcePermission(ResourcePermissionLocalServiceImpl.java:1745)
  at com.liferay.portal.service.impl.ResourcePermissionLocalServiceImpl
    .addResourcePermission(ResourcePermissionLocalServiceImpl.java:311)

Basically, at the point when their UpgradeProcess was executing, it expected that the roles-admin-web portlet module was done deploying, including processing the resource-actions/default.xml file where the ResourceActions were defined.

In an already running portal, this is a fine expectation to make because Liferay would have processed that during the first launch.

But when you do this while Liferay is starting the first time, there isn't really a way in Liferay to wait on data, only for other services or components.

Waiting for Data

So what the community member needed was a way to [possibly] wait for the data to be available.

There's two scenarios to take into account:

  1. The portal has already started and the data is already available, we are good to proceed immediately.
  2. The portal is launching the first time, the data isn't yet available, but when it is added we are good to go.

So I actually came up with a solution for this, but I started from item #2 - how do we know when data is added? Well, a ModelListener can do this for us...

So I started my new ModelListener implementation:

@Component(
  immediate = true,
  service = ModelListener.class
)
public class RoleAdminPortletViewResourceActionModelListener 
    extends BaseModelListener<ResourceAction> 
    implements ModelListener<ResourceAction> {

  @Override
  public void onAfterCreate(ResourceAction model) 
      throws ModelListenerException {
    if (model.getName().equals(RolesAdminPortletKeys.ROLES_ADMIN) && 
       (model.getActionId().equals(ActionKeys.VIEW))) {
      // our resource action has been created...
    }
  }
}

This will tell me when the resource action is available, so now I need to notify it is ready. I decided that I'd borrow a page from Liferay and use a custom ModuleServiceLifecycle, so I would need to register a new service:

private BundleContext _bundleContext;
private ServiceRegistration<ModuleServiceLifecycle>
  _serviceRegistration = null;

@Activate
protected void activate(BundleContext bundleContext) {
  _bundleContext = bundleContext;
}

protected void registerResourceActionAvailable() {
  if (_serviceRegistration != null) return;
	
  _serviceRegistration = _bundleContext.registerService(
    ModuleServiceLifecycle.class, new ModuleServiceLifecycle() { },
      HashMapDictionaryBuilder.<String, Object>
         put("module.service.lifecycle", "roles.admin.view.resource.action.available")
        .put("service.vendor", ReleaseInfo.getVendor())
        .put("service.version", ReleaseInfo.getVersion())
        .build()
  );
}

@Deactivate
protected void deactivate() {
  if (_serviceRegistration == null) return;
	
  _serviceRegistration.unregister();
  _serviceRegistration = null;
}

With this code ready, I tweaked the onAfterCreate() method just a bit:

@Override
public void onAfterCreate(ResourceAction model) 
    throws ModelListenerException {
  if (_serviceRegistration != null) {
    // already found the data, nothing to do here
    return;
  }
  
  if (model.getName().equals(RolesAdminPortletKeys.ROLES_ADMIN) && 
     (model.getActionId().equals(ActionKeys.VIEW))) {
    // our resource action has been created...
    registerResourceActionAvailable();
  }
}

So now, when the data was added, my new ModuleServiceLifecycle instance would be registered and I'd know the data was ready, so I've completely handled case #2.

All that was remaining was case #1, and I solved that with a change to my activate() method:

@Reference
private ResourceActionLocalService _resourceActionLocalService;

@Activate
protected void activate(BundleContext bundleContext) {
  _bundleContext = bundleContext;
	
  ResourceAction rsrcAction = _resourceActionLocalService
    .fetchResourceAction(RolesAdminPortletKeys.ROLES_ADMIN, 
      ActionKeys.VIEW);
	
  if (rsrcAction != null) {
    // data already exists
    registerResourceActionAvailable();
  }
}

In the activate() method I just do a quick check to see if the resource action has already been added. If it has, I know I can immediately register as available, and this covers case #1.

Listening for the Lifecycle Event

So this part is very much like the Power of Patience blog, except I'm waiting on a custom ModuleServiceLifecycle, so it is just a tiny bit more verbose.

In the UpgradeProcess implementation, adding the following will wait for the availability of the data:

@Reference(
  target = "(module.service.lifecycle=roles.admin.view.resource.action.available)", 
  unbind = "-")
protected void setModuleServiceLifecycle(
  ModuleServiceLifecycle moduleServiceLifecycle) { }

Conclusion

So as in the Power of Patience blog, we can use a ModuleServiceLifecycle to pause the activation of our component until the service has been registered, and here we're using a custom service and a custom trigger (the ModelLIstener) to register the service.

Granted having to wait for a specific piece of portal data is a unique case, one that should not happen often.

More often than not, waiting for PORTAL_INITIALIZED is going to be enough for most of your delaying tactics.

However, there are cases where you may want to wait for some sort of event (i.e. connecting to a remote database, receiving data from a remote web service, a file available on the filesystem, etc) before one of your components can do its thing.

The pattern I've laid out here, registering a custom ModuleServiceLifecycle and then @Reference it is one that you can use to support this kind of waiting.

Blogs

I think to use "@Reference " you will need to make "UpgradeProcess" a component. "activate()" of the component won't be called until the "@Reference" is resolved.  

However, if UpgradeProcess is part of a hook, using "upgrade.processes" property configuration, it will be called differently -- its doUpgrade() is called regardless if the "component" is ready or not, no waiting. When the hook's upgrade process is triggered, a different intance of the "UpgradeProcess" will be created and its doUpgrade() is called.

It would be great if your approach  works for a UpgradeProcess in a hook. Do you have any waiting sample implementation?

I think there may be some confusion stemming from how I interspersed the code into the blog, but yes this is one @Component component, the implementation of the ModelListener interface.

It's not at all part of an UpgradeProcess implementation. This code runs at every node so it can either a) create the missing role or b) find the existing role and then signal to all other modules that might need this role to be successful, that the role is ready to go.

With respect to your request about UpgradeProcesses as hooks, we have long abandoned using hooks in Liferay 7, and you should too. Even if they might be working from a legacy perspective, I would consider them as deprecated and likely to go away at any time, and likely aren't tested well at all.

Stick with building UpgradeProcesses built the OSGi way and avoid the hook approach.

 

Thank you David for your reply.

There were two reasons I am still using a Hook:

1. I needed to override portal's term_of_use.jsp. Not sure what would be the new way to do that in Liferay 7. Appreciate if you could advise.

2. I wanted to "update" a lot of things after Liferay is initiated, for example, create a bunch of new roles, new users and articles.  What roles, users and articles to create are based on a configuration file (xml). The code to create these artifacts are in a separate library/OSGI bundle (so it can be reused for different Portal instance/installation). So the configuration will have to be provided by a different bundle or bundle fragment.

There are some challenges:

1) I tried to use the "upgrade.processes" property with the hook. It works only if I deploy the plugin/hook after the portal is up and running, otherwise, some services might not be available. Your blog explained that problem.  Depends on when liferay runs the UpgradeProcess, I would run into different problems. One happens often than others is that the articles I added won't show in the portal because various reasons. One is it's not indexed. I tried to index them in the UpgradeProcess, but many times the indexer for Articles aren't initialized -- even though I have included the "JournalArticleLocalService" in "required-capability" and its package in "import-package";

2) then I explored the idea to use a service/component  of UpgradeStepRegistrator.  This approach has many of its own challengs:

2.1) when I start from a brand new Portal installation, there isn't a record in Release_ table for the bundle, so the UpgradeProcess won't run at all during the portal start up except if I manipulate the "Bundle-Version" and from/to schemaVersion in the register() method. But I haven't found what logic Liferay uses to determine trigger/ or-not-trigger the registered UpgradeProcesses. Although Liferay does not trigger those registered UpgradeProcess, often time I can manually execute them from Gogo shell.

2.2) it's difficult for the registered UpgradeProcess to find the configuration xml during the time doUpgrade() is called. I have to pass the configuration to the UpgradeProcess during the time of regster(). However, the configuration refers to other dynamic configuration that can only be resolved during the time doUpgrade() is called.  The UpgradeProcess class is loaded by a different classloader in another OSGI bundle, so there are many classloader issues.  

Sorry it's a long writing. I would stop here for now. If you have some insights, that would be greatly appreciated.

1. A TOU override hasn't been necessary for years. You can use a web content in lieu of the TOU to avoid the JSP override. And even if you did want to go that route, it's just a JSP bag implementation to provide the alternative. Either way, no hook.

2. What you are describing is easily solved using a Site Initializer, an Upgrade Process, and even a Batch process.

All of the challenges that you've listed stem from maybe not asking for help how to build these things. Had you started by asking what the best approach was (either by posting on Ask or in the community slack), somebody would have pointed you at the right way to do these things that would have avoided the challenges altogether.

Unfortunately it sounds like you decided on a bad implementation path (a hook), dug a hole and continued to dig it deeper and deeper, finding ways to justify continuing with the hook when it was just one bad decision after another.

1. The project started several years back, when a lot things I wanted to do were not provided by Liferay, or not known to me. Over the years Liferay had added many features, but my development has not kept up with liferay's evolution. And each liferay upgrade meant more changes/development.  I also was trying to salvage some work done already.

For example, for TOU, (years back) , I wanted to support multiple languages dynamically; and for each non-English language, I wanted to display content for that language and English version together.  The content itself is now in Web Content.

it's a long story. It's probably the time to drop the hook altogether. 

2. A dedicated OSGI module for UpgradeProcess that bundles code and configuration in one single OSGI bundle would work fine. But as I mentioned previously,  I was trying to put the (generic) code (UpgradeProcess) in one bundle (so it can be reused for different portal instances), and configuration and UpgradeProcessRegistrator code  in another bundle/hook.  That caused many troubles.  I thought even if I replaced Hook with a plain OSGI bundle, if the UpgradeProcess code is in one bundle and the configuration is in another bundle, some of the challenges would remain, right? For example, the UpgradeProcess would still have problem to load the configuration file (which is in another bundle) unless I find away to solve the classloader issue.  Bundle fragment might solve this classloader issue (I tried for a short time though). Any advice on this?

I have never used Site Initializer or Batch Process, but certainly would like to explore. Thanks again for your reply, much appreciated.