Best Practices for Creating OSGi Scheduler in AEM

Problem statement:

The article aims to provide guidelines for creating OSGi schedulers in Adobe Experience Manager, addressing the following queries:

  • What is new with the AEM scheduler with OSGi R7/8 annotations?
  • How to use the scheduler service?
  • Why is enable/disable important on a scheduler?

Requirement:

The article describes the best practices for creating OSGi Schedulers in Adobe Experience Manager (AEM). A scheduler is used to schedule time/cron-based jobs in AEM, and it executes the jobs. The article provides information on creating a scheduler, providing an enabled boolean attribute to start or stop the scheduler, and using the @Activate and @Modified methods. The article also provides a template to create an AEM scheduler.

Introduction:

A scheduler to schedule time/cron based jobs. A job is an object that is executed/fired by the scheduler. The object should either implement the Job interface or the Runnable interface. A job can be scheduled either by creating a ScheduleOptions instance through one of the scheduler methods and then calling schedule(Object, ScheduleOptions) or by using the whiteboard pattern and registering a Runnable service with either the PROPERTY_SCHEDULER_EXPRESSION or PROPERTY_SCHEDULER_PERIOD property. If both properties are specified, only PROPERTY_SCHEDULER_PERIOD is considered for scheduling. Services registered by the whiteboard pattern can by default run concurrently, which usually is not wanted. Therefore it is advisable to also set the PROPERTY_SCHEDULER_CONCURRENT property with Boolean.FALSE. Jobs started through the scheduler API are not persisted and are not restarted after a bundle restart. If the client bundle is stopped, the scheduler will stop all jobs started by this bundle as well. However, the client bundle does not need to keep a reference to the scheduler service.

Create Scheduler Config – OCD

Create a package for config for adding Scheduler related OCD
Creating separate configs will help in the long run if more configs are required for the scheduler

Things to keep in mind:
  1. Always provide an enabled boolean attribute to start or stop the scheduler (sometimes the scheduler takes a long time to run hence this helps to remove those schedulers)
  2. Add the scheduler based on the condition in @Activate, @Modified method
package com.mysite.core.schedulers.config;

import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.AttributeType;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;

@ObjectClassDefinition(name="A scheduled task", description = "Simple demo for cron-job like task with properties")
public @interface SimpleScheduledTaskConfig {

    @AttributeDefinition(name = "Cron-job expression")
    String schedulerExpression() default "*/30 * * * * ?";

    @AttributeDefinition(name = "Concurrent task", description = "Whether or not to schedule this task concurrently")
    boolean schedulerConcurrent() default false;

    @AttributeDefinition(name = "A parameter", description = "Can be configured in /system/console/configMgr")
    String myParameter() default "";

    @AttributeDefinition(name = "Enabled", description = "True, if scheduler service is enabled", type = AttributeType.BOOLEAN)
    public boolean enabled() default true;
}

Creates Scheduler

Create a scheduler using OSGi Component Service DS with a service that has runnable

Reference Scheduler service and sling setting to make sure the scheduler runs only in the author is recommended and override the run method

Make sure the scheduler runs in

  • author mode during @Activate @Modified method
  • get the class’s simple name and use it as a scheduler ID
@Activate
@Modified
protected void activate(SimpleScheduledTaskConfig simpleScheduledTaskConfig) {
  if (isAuthor()) {
    /**
     * Creating the scheduler id
     */
    this.schedulerJobName = this.getClass().getSimpleName();
    addScheduler(simpleScheduledTaskConfig);
    this.myParameter = simpleScheduledTaskConfig.myParameter();
  }
}

Add the scheduler to the scheduler service

private void addScheduler(SimpleScheduledTaskConfig simpleScheduledTaskConfig) {
  /**
   * Check if the scheduler is enabled
   */
  if (simpleScheduledTaskConfig.enabled()) {

    /**
     * Scheduler option takes the cron expression as a parameter and run accordingly
     */
    ScheduleOptions scheduleOptions = scheduler.EXPR(simpleScheduledTaskConfig.schedulerExpression());

    /**
     * Adding some parameters
     */
    scheduleOptions.name(schedulerJobName);
    scheduleOptions.canRunConcurrently(simpleScheduledTaskConfig.schedulerConcurrent());

    /**
     * Scheduling the job
     */
    scheduler.schedule(this, scheduleOptions);

    logger.info("{} Scheduler added", schedulerJobName);
  } else {
    logger.info("Scheduler is disabled");
    removeScheduler();
  }
}

Remove the scheduler if the scheduler is disabled

/**
 * This method removes the scheduler
 */
private void removeScheduler() {
  logger.info("Removing scheduler: {}", schedulerJobName);
  /**
   * Unscheduling/removing the scheduler
   */
  scheduler.unschedule(String.valueOf(schedulerJobName));
}

Use the below template to create the AEM scheduler

package com.mysite.core.schedulers;

import org.apache.sling.commons.scheduler.ScheduleOptions;
import org.apache.sling.commons.scheduler.Scheduler;
import org.apache.sling.settings.SlingSettingsService;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.metatype.annotations.Designate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.mysite.core.schedulers.config.SimpleScheduledTaskConfig;
import com.mysite.core.services.ExampleService;

/**
 * A simple demo for cron-job like tasks that get executed regularly.
 * It also demonstrates how property values can be set. Users can
 * set the property values in /system/console/configMgr
 */
@Component(service=Runnable.class)
@Designate(ocd= SimpleScheduledTaskConfig.class)
public class SimpleScheduledTask implements Runnable {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    /**
     * Id of the scheduler based on its name
     */
    private String schedulerJobName;

    @Reference
    private Scheduler scheduler;

    @Reference
    private SlingSettingsService slingSettings;

    @Reference
    private ExampleService exampleService;

    private String myParameter;
    
    @Override
    public void run() {
        logger.debug("SimpleScheduledTask is now running, myParameter='{}'", myParameter);
        exampleService.generateContentList(myParameter);
    }

    @Activate
    @Modified
    protected void activate(SimpleScheduledTaskConfig simpleScheduledTaskConfig) {
    	if (isAuthor()) {
            /**
             * Creating the scheduler id
             */
            this.schedulerJobName = this.getClass().getSimpleName();
            addScheduler(simpleScheduledTaskConfig);
            this.myParameter = simpleScheduledTaskConfig.myParameter();
        }
    }

    private void addScheduler(SimpleScheduledTaskConfig simpleScheduledTaskConfig) {
        /**
         * Check if the scheduler is enabled
         */
        if (simpleScheduledTaskConfig.enabled()) {
            /**
             * Scheduler option takes the cron expression as a parameter and run accordingly
             */
            ScheduleOptions scheduleOptions = scheduler.EXPR(simpleScheduledTaskConfig.schedulerExpression());
            /**
             * Adding some parameters
             */
            scheduleOptions.name(schedulerJobName);
            scheduleOptions.canRunConcurrently(simpleScheduledTaskConfig.schedulerConcurrent());
            /**
             * Scheduling the job
             */
            scheduler.schedule(this, scheduleOptions);
            logger.info("{} Scheduler added", schedulerJobName);
        } else {
            logger.info("Scheduler is disabled");
            removeScheduler();
        }
    }

    /**
     * This method removes the scheduler
     */
    private void removeScheduler() {
        logger.info("Removing scheduler: {}", schedulerJobName);
        /**
         * Unscheduling/removing the scheduler
         */
        scheduler.unschedule(String.valueOf(schedulerJobName));
    }

    /**
     * It is use to check whether AEM is running in Publish mode or not.
     * @return Returns true is AEM is in publish mode, false otherwise
     */
    public boolean isAuthor() {
        return this.slingSettings.getRunModes().contains("author");
    }
}

Best Practices for Writing OSGi Services Using R7/R8 Annotations

Problem Statement:

What is the best way to write an OSGi service?

Requirement:

What is the best way to write an OSGi service? What is the recommended way to register a service, as per R7/R8 annotations?

Introduction:

A Service Component is a Java class that can be optionally registered as an OSGi service and can optionally use OSGi services. The runtime for DS is called Service Component Runtime (SCR) and uses component descriptions in the bundle (in XML form) to instantiate and wire components and services together. The component descriptions are used by SCR as a performance choice so that SCR can avoid processing all classes in a bundle to search for annotations at runtime

As per Adobe’s best practices please start using OSGi R7/R8 annotations, avoid using felix based component annotations

OSGi R7/8

Create Services using Service Component

A Service Component is a Java class that can be optionally registered as an OSGi service and can optionally use OSGi services. The runtime for DS is called Service Component Runtime (SCR) and uses component descriptions in the bundle (in XML form) to instantiate and wire components and services together. The component descriptions are used by SCR as a performance choice so that SCR can avoid processing all classes in a bundle to search for annotations at runtime

Create Example Service – ExampleService

package com.mysite.core.services;

import java.util.Set;

public interface ExampleService {
    /**
     * Start generation content list
     *
     * @return result set
     */
    public Set<String> generateContentList(String path);
}

Create Example Service Object class definition – ExampleServiceConfig

  1. Creating a separate Interface annotation class will help in the long run when we want to introduce more fields
  2. Service config name and description
  3. Attribute name, description, and type
package com.mysite.core.services.config;

import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.AttributeType;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;

@ObjectClassDefinition(name = "Example Content list generator Service", description = "Example Content list generator Service generates Set")
public @interface ExampleServiceConfig {

    @AttributeDefinition(name = "Enabled Generator", description = "True, to generate the list", type = AttributeType.BOOLEAN)
    boolean isEnabled() default false;
}

Create Service Implementation – ExampleServiceImpl

  1. Get OCD based configs @Activate without any method
  2. You can also get config inside @Modfied with the method
  3. To get a resource resolver inside the service it’s recommended to use “try-with-resource” which helps to never forget to close a resource(avoids unclosed resource resolver warnings)
package com.mysite.core.services.impl;

import com.mysite.core.services.ExampleService;
import com.mysite.core.services.config.ExampleServiceConfig;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.metatype.annotations.Designate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

@Component(service = ExampleService.class, immediate = true, name = "Example Content Generator Service")
@Designate(ocd = ExampleServiceConfig.class)
public class ExampleServiceImpl implements ExampleService {

    private static final Logger LOGGER = LoggerFactory.getLogger(ExampleServiceImpl.class);
    /** Service User name */
    private static final String SERVICE_USER = "exampleUset";
    private boolean serviceEnabled;

    @Reference
    private ResourceResolverFactory resolverFactory;

    //R8 Annotation
    @Activate
    ExampleServiceConfig exampleServiceConfig;

    //R7 Annotation but captures and @Modified configs
    @Modified
    protected void activate(final ExampleServiceConfig exampleServiceConfig) {
        this.exampleServiceConfig = exampleServiceConfig;
    }

    @Override
    public Set<String> generateContentList(String path) {
        if(exampleServiceConfig.isEnabled()){
            try (ResourceResolver resourceResolver = resolverFactory.getServiceResourceResolver(Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, SERVICE_USER))){

            } catch (LoginException e) {
                LOGGER.error("Error Occured during Login", e.getMessage());
            }
        }
        return new HashSet<>();
    }
}

Project Structure

Code structure for best practices

Note: Make sure to add the config XML’s for the services and please don’t depend on the OCD default values.