Expand my Community achievements bar.

SOLVED

OSGI dynamic scheduler + configurationFactory

Avatar

Level 3

Hi Experts, 

I am running into the issue where I am trying to add dynamic jobs to the scheduler. 

Here is my setting/scenario:

1. FactoryConfigService.java -
@Component(configurationFactory = true, policy = ConfigurationPolicy.REQUIRE, immediate = true, label = "Factory Config Service", description = "Factory configuration Service", metatype = true)@Properties({ 
@Property(name = Constants.SERVICE_DESCRIPTION, value = "Factory Configuration Service"),
@Property(name = Constants.SERVICE_VENDOR, value = "FactoryConfig") })
@Service(value = FactoryConfigService.class)

  • It contains a property 

        @Property(label = "cronJob", value = "cronJob", description = "cronJob"
        public static final String SITE_NAME = "cronJob";

  • I created 3 instances of "Factory Config Service" from OSGI console and set cronJob to be 0 0 1 * * ?, 0 0 2 * * ? and 0 0 3 * * ?

2. Scheduler.java

@Component(immediate = true, metatype = true, label = Scheduler")
@Properties({
@Property(name = Constants.SERVICE_DESCRIPTION, value = "Scheduler"),
@Property(name = Constants.SERVICE_VENDOR, value = "Universal") })
@Service(value = Scheduler.class)

@Reference(policy = ReferencePolicy.STATIC)   
 
private FactoryConfigService configService;  // Reference to the Factory config

@Reference(policy = ReferencePolicy.STATIC)
 private ConfigurationAdmin configAdmin;

protected void activate(ComponentContext componentContext) throws Exception {

LOGGER.info("Scheduler in activation: " + this.scheduler);

//pseudocode
// Add jobs

}

GOAL:  I would like to execute activate method of Scheduler every time I add new instance config or modify existing instance.

PROBLEM: activate method gets invoked when bundle is restarted, refreshed and deployed, BUT does not get called again when I add new instance or modify new instance ( change cronJob schedule)

Any pointers or sample framework code would be greatly appreciated. 

1 Accepted Solution

Avatar

Correct answer by
Former Community Member

Hello cqlearner,

How could you expect the activate method of Scheduler to be called on every config add/change in FactoryConfigService, both are separate OSGI components and have their own lifecycle.

Now I am explaining a couple of ways to solve it.

1. Via ManagedServiceFactory - Recommended One.

/** * SchedulerManagedServiceFactory * * @author Rakesh.Kumar, NextEon Solutions. */ @Service(ManagedServiceFactory.class) @Component(immediate = true, metatype = true, label = "SchedulerManagedServiceFactory", configurationFactory = true) public class SchedulerManagedServiceFactory implements ManagedServiceFactory { /** * Map that will hold the Managed Instances. */ private final ConcurrentMap<String, ManagedScheduler> managedSchedulers = new ConcurrentHashMap<>(); /** * Configurable Property cronExpr. */ @Property( label = "Cron Expression", description = "Cron Expression for the job : See - http://docs.oracle.com/cd/E12058_01/doc/doc.1014/e12030/cron_expressions.htm") private static final String CRON_EXPRESSION = "cronExpr"; /** * Sling Scheduler. */ @Reference private Scheduler scheduler; @Override public String getName() { return "Simple Scheduler ManagedServiceFactory"; } @Override public void updated(String pid, Dictionary<String, ?> properties) throws ConfigurationException { String cronExpr = PropertiesUtil.toString(properties.get(CRON_EXPRESSION), ""); ManagedScheduler managedScheduler = this.managedSchedulers.get(pid); // Means a new configuration. if (managedScheduler == null) { // Create a new one and put into the managedSchedulers map SchedulerConfig schedulerConfig = new SchedulerConfig(cronExpr); ManagedScheduler ms = new ManagedScheduler(this.scheduler, schedulerConfig); boolean isScheduled = ms.schedule(pid, schedulerConfig); if (isScheduled) { this.managedSchedulers.put(pid, ms); } else { // Take you action } } else { // configuration already exists. // Check if cronExpr changed, you may want to re-schedule it. } } @Override public void deleted(String pid) { ManagedScheduler managedScheduler = this.managedSchedulers.remove(pid); if (managedSchedulers != null) { managedSchedulers.unschedule(pid); } } } /** * @author Rakesh.Kumar, NextEon Solutions. */ public class ManagedScheduler { // Sling Scheduler - org.apache.sling.commons.scheduler.Scheduler private Scheduler scheduler; // To maintain the configs from SchedulerManagedServiceFactory. private SchedulerConfig config; public ManagedScheduler(Scheduler scheduler, SchedulerConfig config) { this.scheduler = scheduler; this.setConfig(config); } public boolean schedule(String jobName, SchedulerConfig config) { ScheduleOptions scheduleOptions = this.scheduler.EXPR(config.getCronExpr()); scheduleOptions.name(jobName); Map<String, Serializable> configMap = new HashMap<>(); configMap.put("blah", "blah"); scheduleOptions.config(configMap); return this.scheduler.schedule(new SimpleJob(), scheduleOptions); } public boolean unschedule(String pid) { return this.scheduler.unschedule(pid); } } /** * Simple Sling Job Implementation to be passed to a scheduler. * * @author Rakesh.Kumar, NextEon Solutions. */ public class SimpleJob implements org.apache.sling.commons.scheduler.Job { /** * Execute this job. * * @param context *            - The context of the job. */ @Override public void execute(JobContext jobContext) { // Do your stuff here. } } /** * @author Rakesh.Kumar, NextEon Solutions. */ public class SchedulerConfig { private String cronExpr; // Other configs /** * @param cronExpr */ public SchedulerConfig(String cronExpr) { this.cronExpr = cronExpr; } /** * @return the cronExpr */ public String getCronExpr() { return cronExpr; } /** * @param cronExpr *            the cronExpr to set */ public void setCronExpr(String cronExpr) { this.cronExpr = cronExpr; } }

2. Via ComponentFactory

/** * ComponentFactory * * @author Rakesh.Kumar, NextEon Solutions. */ @Component(factory = "scheduler.componentInstance") public class SchedulerComponent { @Activate protected void activate(ComponentContext context) { Dictionary<?,?> props = context.getProperties(); // do with configs } // More relevant methods. Like schedule/unschedule } // Now in you FactoryConfigService Inject a ComponentFactory which get the factory component registered above. @Reference(target = "(component.factory=scheduler.componentInstance)") private ComponentFactory factory; // Then in activate of FactoryConfigService /** * Map that will hold the ComponentInstance against a key, may be cronExpr. */ private final ConcurrentMap<String, ComponentInstance> componentInstances = new ConcurrentHashMap<>(); @Activate protected void activate(ComponentContext context) { Dictionary<?,?> props = context.getProperties(); this.instance = factory.newInstance(props); ComponentInstance instance = instance.getInstance(); SchedulerComponent comp = (SchedulerComponent)instance; // you may want to call any method on SchedulerComponent here this.componentInstances.put("your key - may be cron itself", instance); } @Deactivate public void deactivate(ComponentContext context) { // You should dispose of all the ComponentInstance this.componentInstances.forEach((cron, compInstance) -> compInstance.dispose()); }

This is the most comprehensive example i could give and hoping this will help you.

Please let me know if you want to discuss anything

Thanks,

Rakesh

View solution in original post

9 Replies

Avatar

Level 10

We have a community article that shows how to build a Scheduler service with AEM: 

Scheduling Adobe Experience Manager Jobs using Apache Sling

When working with scheduling - make sure that your expression is valid:

schedulingExpression = "0 15 10 ? * MON-FRI"

Also - be sure to check the Sling docs on this subject:

http://sling.apache.org/documentation/bundles/scheduler-service-commons-scheduler.html

Avatar

Correct answer by
Former Community Member

Hello cqlearner,

How could you expect the activate method of Scheduler to be called on every config add/change in FactoryConfigService, both are separate OSGI components and have their own lifecycle.

Now I am explaining a couple of ways to solve it.

1. Via ManagedServiceFactory - Recommended One.

/** * SchedulerManagedServiceFactory * * @author Rakesh.Kumar, NextEon Solutions. */ @Service(ManagedServiceFactory.class) @Component(immediate = true, metatype = true, label = "SchedulerManagedServiceFactory", configurationFactory = true) public class SchedulerManagedServiceFactory implements ManagedServiceFactory { /** * Map that will hold the Managed Instances. */ private final ConcurrentMap<String, ManagedScheduler> managedSchedulers = new ConcurrentHashMap<>(); /** * Configurable Property cronExpr. */ @Property( label = "Cron Expression", description = "Cron Expression for the job : See - http://docs.oracle.com/cd/E12058_01/doc/doc.1014/e12030/cron_expressions.htm") private static final String CRON_EXPRESSION = "cronExpr"; /** * Sling Scheduler. */ @Reference private Scheduler scheduler; @Override public String getName() { return "Simple Scheduler ManagedServiceFactory"; } @Override public void updated(String pid, Dictionary<String, ?> properties) throws ConfigurationException { String cronExpr = PropertiesUtil.toString(properties.get(CRON_EXPRESSION), ""); ManagedScheduler managedScheduler = this.managedSchedulers.get(pid); // Means a new configuration. if (managedScheduler == null) { // Create a new one and put into the managedSchedulers map SchedulerConfig schedulerConfig = new SchedulerConfig(cronExpr); ManagedScheduler ms = new ManagedScheduler(this.scheduler, schedulerConfig); boolean isScheduled = ms.schedule(pid, schedulerConfig); if (isScheduled) { this.managedSchedulers.put(pid, ms); } else { // Take you action } } else { // configuration already exists. // Check if cronExpr changed, you may want to re-schedule it. } } @Override public void deleted(String pid) { ManagedScheduler managedScheduler = this.managedSchedulers.remove(pid); if (managedSchedulers != null) { managedSchedulers.unschedule(pid); } } } /** * @author Rakesh.Kumar, NextEon Solutions. */ public class ManagedScheduler { // Sling Scheduler - org.apache.sling.commons.scheduler.Scheduler private Scheduler scheduler; // To maintain the configs from SchedulerManagedServiceFactory. private SchedulerConfig config; public ManagedScheduler(Scheduler scheduler, SchedulerConfig config) { this.scheduler = scheduler; this.setConfig(config); } public boolean schedule(String jobName, SchedulerConfig config) { ScheduleOptions scheduleOptions = this.scheduler.EXPR(config.getCronExpr()); scheduleOptions.name(jobName); Map<String, Serializable> configMap = new HashMap<>(); configMap.put("blah", "blah"); scheduleOptions.config(configMap); return this.scheduler.schedule(new SimpleJob(), scheduleOptions); } public boolean unschedule(String pid) { return this.scheduler.unschedule(pid); } } /** * Simple Sling Job Implementation to be passed to a scheduler. * * @author Rakesh.Kumar, NextEon Solutions. */ public class SimpleJob implements org.apache.sling.commons.scheduler.Job { /** * Execute this job. * * @param context *            - The context of the job. */ @Override public void execute(JobContext jobContext) { // Do your stuff here. } } /** * @author Rakesh.Kumar, NextEon Solutions. */ public class SchedulerConfig { private String cronExpr; // Other configs /** * @param cronExpr */ public SchedulerConfig(String cronExpr) { this.cronExpr = cronExpr; } /** * @return the cronExpr */ public String getCronExpr() { return cronExpr; } /** * @param cronExpr *            the cronExpr to set */ public void setCronExpr(String cronExpr) { this.cronExpr = cronExpr; } }

2. Via ComponentFactory

/** * ComponentFactory * * @author Rakesh.Kumar, NextEon Solutions. */ @Component(factory = "scheduler.componentInstance") public class SchedulerComponent { @Activate protected void activate(ComponentContext context) { Dictionary<?,?> props = context.getProperties(); // do with configs } // More relevant methods. Like schedule/unschedule } // Now in you FactoryConfigService Inject a ComponentFactory which get the factory component registered above. @Reference(target = "(component.factory=scheduler.componentInstance)") private ComponentFactory factory; // Then in activate of FactoryConfigService /** * Map that will hold the ComponentInstance against a key, may be cronExpr. */ private final ConcurrentMap<String, ComponentInstance> componentInstances = new ConcurrentHashMap<>(); @Activate protected void activate(ComponentContext context) { Dictionary<?,?> props = context.getProperties(); this.instance = factory.newInstance(props); ComponentInstance instance = instance.getInstance(); SchedulerComponent comp = (SchedulerComponent)instance; // you may want to call any method on SchedulerComponent here this.componentInstances.put("your key - may be cron itself", instance); } @Deactivate public void deactivate(ComponentContext context) { // You should dispose of all the ComponentInstance this.componentInstances.forEach((cron, compInstance) -> compInstance.dispose()); }

This is the most comprehensive example i could give and hoping this will help you.

Please let me know if you want to discuss anything

Thanks,

Rakesh

Avatar

Level 3

Thanks!

I will try this out. As I started to read/experiment I understood I can not expect active method to run from separate OSGI. I thought if I have @Referece of my config service it will automatically notify to the scheduler hence call @Active of Scheduler. I guess this only works if config is not a configurationFactory.

Avatar

Level 3

Thank you for helpful links, but my problem is not able to fire activate method of Scheduler when new instance or config is modified in the ConfigurationFactory. It looks like Scheduler gets notified only when bundle is refreshed or installed, NOT when a config is added to the instances of  FactoryConfigService.java in my same code above. 

Avatar

Community Advisor

Try marking your activate method with both Activate and Modified annotations.

Seems to be relevant: https://issues.apache.org/jira/browse/SLING-3422

     

    Avatar

    Level 3

    hi Rakesh,

    I see you have @Property for Cron Expression, but how do I set that from OSGI config? I don't want to hardcode the expressions. Basically my challenge is I have config factory instance in OSGI console and I would like to fire scheduler whenever property gets added/modified/deleted. I am thinking of something like ay.crx.sling.server.impl.jmx.GarbageCollectionConfig

    Avatar

    Former Community Member

    Let me explain few more things. I see you have injected the FactoryConfigService in Scheduler as a static reference policy which is default policy so you can omit policy attribute for STATIC option btw.

    @Reference(policy = ReferencePolicy.STATIC) private FactoryConfigService configService;  // Reference to the Factory config

    This way whenever FactoryConfigService goes away the Scheduler becomes unsatisfied and first the "deactivate" method of Scheduler will be called and then of FactoryConfigService, the activation is just reverse of this i.e. "activate" method of FactoryConfigService will be first called and then of "Scheduler".

    However, activate method for an OSGI component will be called on configuration changes as well if you do not want that behaviour then declare another method with @Modified annotation.

    This is a standard OSGI mechanism and has nothing to do with the "configurationFactory"

    Remember the Map or ComponentContext(there are multiple method signature possible for activate method) passed to activate method holds the configurations of the current component only. So when you are changing configs of FactoryConfigService those are available to the FactoryConfigService component only.

    HTH.

    Thanks,

    Rakesh

    Avatar

    Former Community Member

    Seems you have not gone through the complete code I shared here in details.

    Basically, If you specify a property then that is available in the OSGI console by default, until you declare it "propertyPrivate = true" in your @Property annotation.

    There is no hardcoding at all, in OSGI console populate the cron expression property in your factory config and get that in your activate or updated method. 

    Not sure what is the issue you have with that.

    Just try to follow what I have explained earlier and this will solve your problem.

    Thanks,

    Rakesh

    The following has evaluated to null or missing: ==> liqladmin("SELECT id, value FROM metrics WHERE id = 'net_accepted_solutions' and user.id = '${acceptedAnswer.author.id}'").data.items [in template "analytics-container" at line 83, column 41] ---- Tip: It's the step after the last dot that caused this error, not those before it. ---- Tip: If the failing expression is known to be legally refer to something that's sometimes null or missing, either specify a default value like myOptionalVar!myDefault, or use <#if myOptionalVar??>when-present<#else>when-missing. (These only cover the last step of the expression; to cover the whole expression, use parenthesis: (myOptionalVar.foo)!myDefault, (myOptionalVar.foo)?? ---- ---- FTL stack trace ("~" means nesting-related): - Failed at: #assign answerAuthorNetSolutions = li... [in template "analytics-container" at line 83, column 5] ----