Starting Apex Scheduled Jobs Without Ugly Cron Expressions

Friday, June 24, 2011

tl;dr - I show you how to use a button to start a scheduled job in Salesforce. Skip the first three paragraphs to get to the meat of this post, and lots of code.


Often in Salesforce, an administrator or developer wants to update all the records in their database for some reason. Maybe they are changing the way data is stored in a field, or maybe they are migrating it from one field to another, but what matters is that doing this can take a long time, and Salesforce's multi-tenant architecture does not like long-running processes that reduce the response time important operations, like database triggers.

To solve this problem, Salesforce appeases the developers by providing a way for them to asynchronously run operation at a lower precedence. This is a powerful feature to have, but you have to deal with using ugly (but powerful) Cron expressions to start the process. Note, however, that Salesforce uses a slightly more restrictive Cron expression, so refer to the Salesforce System.schedule documentation instead of the standard Cron documentation.

It's got some serious limitations. The only way to start a scheduled job is by writing Apex code, probably from an anonymous block or from a trigger or Visualforce controller. In a similarly restrictive manner, the easiest way to stop a job once it is scheduled is to go to Setup > Monitoring > Scheduled Jobs in the SF UI. This is not a good long-term solution to starting and stopping jobs.

So let's find a better solution. Let's eliminate the code and turn it into a 'Start' button and allow the user to supply a single number to the expression. Something like this:



Now, clicking on the 'Start' button should take the specified integer, complete the Cron expression, and pass it to the scheduler. Keep in mind that my code is assuming that this page is a snippet from one of my VF pages that uses a standard controller for a Config_Object__c sObject. Here's some example code.

Visualforce - 


<apex:pageBlockSectionItem >
    <apex:outputLabel value="Start Notifications:" for="startNotifications"/>
    <apex:commandButton action="{!startScheduledJob}" value="Start" id="startNotifications"/>
</apex:pageBlockSectionItem>
<apex:pageBlockSectionItem >
    <apex:outputLabel value="Notify on Nth Minute of Each Hour:" for="notificationMinute"/>
    <apex:inputText value="{!notificationMinute}" id="notificationMinute" size="10" maxlength="2"/>
</apex:pageBlockSectionItem>


Controller -

public void setNotificationMinute(String pNotificationMinute) {
             Integer notificationMinute = 0;
     
     try {//cleanse the input
      notificationMinute = Integer.valueOf(pNotificationMinute);
     }
     catch (Exception e) {
      notificationMinute = 0;
     }
     if (notificationMinute == null) {
      notificationMinute = 0;
     }
     if (notificationMinute < 0 || notificationMinute > 59) {
      notificationMinute = 0;
     }
  String notificationSchedule = '0 ' + notificationMinute + ' * * * ?';//and create the Cron expression
  configObject.Notification_Schedule__c = notificationSchedule;
}
public void startScheduledJob() {
     //notificationMinute is a field on the configObject sObject.
                        if (configObject == null) {

      init();
    }
    //Let's construct this: System.schedule('Notification Job', '0 49 * * * ?', new ScheduledEmailJob());
     String notificationSchedule = pConfigObject.Notification_Schedule__c;
     if (notificationSchedule == null)
          notificationSchedule = '0 0 * * * ?';//some default value
      
     if (configObject.Scheduled_Email_Job_Id__c == null) {//start the job if we haven't started it yet
     String jobId = System.schedule('Notification Job', notificationSchedule, new MyNotificationJob());
     configObject.Scheduled_Email_Job_Id__c = (Id) jobId;//Persist the jobId to the database so we can use it to stop the job later.
     }
     else if (configObject.Scheduled_Email_Job_Id__c != null) {
          //delete the existing job and start a job with this new value
          stopScheduledJob(pConfigObject);
          String jobId = System.schedule('Notification Job', notificationSchedule, new ScheduledEmailJob());
          pConfigObject.Scheduled_Email_Job_Id__c = (Id) jobId;
     }
}


Also, it would be nice to have some information about the currently running job, if there is one. This information is stored in the CronTrigger record that was created. The only handle we have on this record is the jobId, which was the return value of the System.schedule method and is the Id of the CronTrigger record that was created.

It'll look something like this on the VF page -


And here's the code behind it -

public String getStatusNotification() {
     String status = '';
     //The only handle we have on the currently running cron job is the jobId that was given to us when we scheduled the job.
     String jobId = configObject.Scheduled_Email_Job_Id__c;
     if (jobId == null) {
      status = 'Notification job not scheduled.';
     }
     else {
      try {
       //Now that we have the jobId, we'll query for the informational record in the CronTrigger record.
       CronTrigger existingCronTrigger = [SELECT State, NextFireTime FROM CronTrigger WHERE Id = :jobId LIMIT 1];
       status = 'State: ' + existingCronTrigger.State + ' - Next Notification Time: ' + existingCronTrigger.NextFireTime;
      }
      catch (DmlException e) {
       status = 'Invalid notification job Id.';
      }
     }
     //Return the status of the currently running job, if there is one, to the page.
     return status;
}


The task I leave you with, simple though it may be, is to add a 'Stop' button that will stop whatever job is currently up in the air. (Hint: try the System.abortJob method)

1 comment:

  1. This comment has been removed by a blog administrator.

    ReplyDelete