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)

The LastModifiedDate Field - Behavior Differences in FeedItem vs. Other sObjects

Tuesday, June 14, 2011

TL;DR - FeedItem.LastModifiedDate does not behave the same way as a normal sObject's LastModifiedDate in, say, a master-detail relationship.


Reading through the Spring '11 release notes (PDF) in preparation for my SF 501 Cert. exam next week, I discovered an oddity. As you can see under the "Changed Chatter Objects" section on page 76, the LastModifiedDate field has been added to Chatter objects. This is great, but the definition of its behavior is what caught my interest. Here's what they say:
When a feed item is created, LastModifiedDate is the same as CreatedDate. If a FeedComment is inserted onthat feed item, then LastModifiedDate becomes the CreatedDate for that FeedComment. Deleting theFeedComment does not change the LastModifiedDate.
Woah, so the LastModifiedDate for a FeedItem changes even when you don't directly interact with that FeedItem? Not the behavior that I would expect to see. Now, I can see this as useful for Chatter feeds because the time of update is the important bit of information here, but isn't this an exception to the rule? That is, if I create two sObjects, one called 'Master' and one called 'Detail' with a master-detail relationship between the two, will the addition of a new Detail record update the LastModifiedDate of the Master record?

I would guess that it wouldn't, but I couldn't find any documentation on it, so I went to my personal dev org to test it out.

  1. I defined a 'Master' object and a 'Child' object with a master-detail relationship between them.
  2. I created a new 'Master' record.
    1. (Master's LastModifiedDate == 6/13/2011 2:45 PM)
  3. I created a new 'Detail' record on that 'Master' record.
    1. (Detail's LastModifiedDate == 6/13/2011 2:47 PM)
    2. (Master's LastModifiedDate == 6/13/2011 2:45 PM)
According to my results, adding a detail record to a master record will not change the master record's LastModifiedDate. Chatter's FeedItem.LastModifiedDate is the exception, so keep this in mind.

Easy To Write In Apex, But Not In Javascript? Use Javascript Remoting!

Thursday, June 9, 2011

New in the Summer '11 release of Salesforce.com is the GA of the shiny new Javascript Remoting. To quickly define it, Salesforce now provides the ability for you to write Javascript code on a Visualforce page that references a static method on that page's controller.

Now, there has always been a way for a Visualforce page to talk to its controller, by way of VF's actionFunction component as one among other built-in AJAX functionality, but Javascript remoting is a bit different. As its doc page tells about the differences between the two: Javascript remoting allows you to pass parameters to an Apex method and allows you to specify a Javascript callback function, whereas the actionFunction component allows you to specify rerender targets on the page and submits the page's entire form back to the controller. By communicating with the controller with the former, a light-weight JSON object is passed back and forth, but when using actionFunction, the page's entire form is serialized, sent across the wire, and deserialized again, which takes a significant amount of time. One is not inherently better than the other, but they each have their own use cases. This post is about the first use case that I found for JS remoting.

Problem

Challenge: Dynamically update the End Date when changing either Work Days or Start Date.
After estimating the time and dollar cost of a project, our process requires us to log our estimates into our Salesforce org. I was the lucky one chosen to implement the SF-side of the solution. I like to provide a friendly and intuitive user experience, so I wanted to make it very Ajax-y. More specifically, I wanted a way to  instantly update dependent values when the user enters the controlling value. This is easy with jQuery when it's just numbers and sums, but when it comes to computing dates, Javascript's client-side capabilities falter (unless a JS pro can inform me otherwise).

Solution

I had already solved this problem in Apex, for calculating this value when the record is submitted, by creating a method called getEndDateAfterWorkdays, which takes a Date startDate and an Integer workDays as arguments. I'm sure it's possible to do in Javascript, but I'm not a pro with the language and I didn't really want to duplicate the logic there (see DRY), so I looked for other solutions. Luckily for me, the Summer '11 release was just around the corner, which would allow me to use JS remoting to call the method I already defined in Apex!

The Code

Here's how it works.

1) Attach an onChange Javascript event listener to the Work Days and Start Date input fields on the VF page that will call the Javascript remoting function.

$j(".duration").live("keyup", function(){ updateEndDate(); });
$j(".startDate").live("change", function(){ updateEndDate(); });


//using javascript remoting, send the startDate and the duration to the server, get the endDate back
function updateEndDate() {
$j(".startDate").each(function() {
//Get the necessary info from each row in the list.
var startDate = $j(this).parent().parent().parent().find(".startDate").val();
var duration = $j(this).parent().parent().parent().find(".duration").val();
var endDateId = $j(this).parent().parent().parent().find(".endDate").attr("id");
//Remotely call the controller method.
CtlrEstimateEdit.getEndDateAfterWorkdays( startDate, duration, endDateId, function(result, event) {
//This callback function doesn't remember the row from which it was called.
// My solution: pass the Id of the destination element into the function and pass it back into this callback.
var resultArray = result.split(",");
var resultEndDate = resultArray[0];
var resultSpanToInsertId = resultArray[1];
if (event.status) {//if successful
var spanToInsertIdEsc = esc(resultSpanToInsertId);
$j(spanToInsertIdEsc).html(resultEndDate);
}
});
});
}


2) Add the remoting method to the controller for the Javascript to hit.

@RemoteAction
global static String getEndDateAfterWorkdays(String pStartDate, Integer pWorkDays, String pEndDateIdToPass) {
Date startDate = Date.parse(pStartDate);
Date endDate = startDate;
Integer durationInCalendarDays = HlprLaborEstimateMasterTrigger.workDaysDurationToCalendarDaysDuration(pWorkDays - 1, startDate);//Minus 1 to start working that day
endDate = startDate.addDays(durationInCalendarDays);
//now convert back to a string to send back to the javascript
DateTime endDateTime = DateTime.newInstance(endDate, Time.newInstance(0, 0, 0, 0));
String endDateToReturn = endDateTime.format('MM/dd/yyyy');
//add the endDateId of the span to which to write this new value. I couldn't figure out another way but to push this value through here
String endDatePlusSpanIdToReturn = endDateToReturn + ',' + pEndDateIdToPass;
return endDatePlusSpanIdToReturn;
}


That's it, really, unless I forgot to paste some code here. (Let me know if I did, please.)

Besides enforcing the DRY coding principle, what other use cases have you found for using Javascript remoting?

Adding Visualforce pages to the Home Tab by Using HtmlArea Components

Tuesday, June 7, 2011

The Salesforce home tab is a pretty great place to start your day...IF you take the time to set it up and personalize it to your needs. This the stage I'm at - trying to get useful information onto my homepage.  I've decided to make a Visualforce page that will display a list of items for me to look at for the current day. I'll take this custom page and display it in a home page component of type HtmlArea. Take a look at this Force.com blog post to see the direction of my intentions, except I'm using an iframe to display my VF page. I was able to knock a simple page pretty quickly. The tricky part, however, was coercing the home tab page to display it in its full glory.

<iframe src="/apex/ItemsForToday?isComp=1&showAll=0" frameborder="0" width="100%"  height="300px"></iframe>

Here you can see the HTML code that I used to embed my Visualforce page into the home tab component of type HtmlArea. After saving this, I went back to my home tab to see my new product. Sadly, it only gave my component maybe 60px of height! (scrollbar-ing the rest) What happened?

After investigation, it seems that there is a Salesforce CSS style that is overriding the height attribute on this iframe. The quick and easy solution that I came up with is this:

<iframe src="/apex/TaskUserPrioritiesManager?isComp=1&amp;showAll=0" frameborder="0" width="100%" height="900" style="height:315px;"></iframe>

Just add a CSS style directly on the iframe tag. This style has a more specific scope than the one Salesforce is using, so ours takes precedence. Now, when returning to the home tab, I see that my VF page is looking proud on my homepage.