Queueable Apex: More Than an @future

The Salesforce Winter '15 platform release brought us the new Queueable Apex interface. This interface is a cool new way to execute asynchronous computations on the Force.com platform, given you already know @future, Scheduled Apex Jobs, and Batch Jobs. Here's a practical use case.

The Winter ’15 platform release brought us the new Queueable Apex interface. This interface is a cool new way to execute asynchronous computations on the Force.com platform, given you already know @future, Scheduled Apex Jobs, and Batch Jobs.

The main differences between @future methods and Queueable Apex jobs are:

  1. When you enqueue a new job, you get a job ID that you can actually monitor, like batch jobs or scheduled jobs!
  2.  You can enqueue a queueable job inside a queueable job (no more “Future method cannot be called from a future or batch method” exceptions). As for Winter ’15 release, you can chain a maximum of two queueable jobs per call (so a job can fire another job and that’s it!). With Spring ’15 release, this limit has been removed.
  3.  You can have complex Objects (such as SObjects or Apex Objects) in the job context (@future only supports primitive data types)

All I want to do in this article is show a practical use case for this interface (for those impatients out there, the complete code of this article can be found here).

Business requirement: You have to send a callout to an external service whenever a Case is closed.

Constraints:  The callout will be a REST POST method that accepts a JSON body with all the non-null Case fields that are filled exactly when the Case is closed (the endpoint of the service will be a simple RequestBin).

The Queueable Apex Use Case

Using a future method, we will pass the case ID to the job and so make a subsequent SOQL query: this is against the requirement to pass the fields we have in the Case at the exact time of the update. This might seem an excessive constraint, but with big ORGs and hundreds of future methods in execution (due to system overload), future methods can actually be executed after minutes,  so the Case state can be different from when the future was actually fired.

To store the attempts of callout (and the responses, this is only a helper method that allows for reportization of the attempts) we will use a new SObject called Callout__c with the given fields:

– Case__c: master/detail on Case

– Job_ID__c: external ID / unique / case sensitive, stores the queueable job ID

– Sent_on__c: date/time, when the callout took place

– Duration__c: integer, milliseconds for the callout to be completed (we can report timeouts easily)

– Status__c: picklist, valued are Queued (default), OK (response 200), KO (response != 200) or Failed (exception)

– Response__c: long text, stores the server response

 To achive the Business needs, we need a Case trigger:

trigger CaseQueueableTrigger on Case (after insert, after update) {

    List calloutsScheduled = new List();
    for(Integer i = 0; i 0){
        insert calloutsScheduled;
    }
}

The trigger iterates bulkily through the trigger’s cases and if they are created as “Closed” or the Status field changes to “Closed,” a new job is enqueued and a Callout__c object is added to the list that will be inserted outside the “for.”

This way we always have evidence on the system that the callout has been fired.

Remember that you can add up to 50 jobs to the queue with System.enqueueJob in a single transaction, so you have to be sure that the trigger makes a maximum of 50 “System.enqueueJob” invocations (this is up to you!).

Let’s have a look at the job class:

public class CaseQueuebleJob implements Queueable, Database.AllowsCallouts {
. . .
}

The Queueable interface is the main reason of this article, while the Database.AllowsCallouts allow us to send a callout inside the job.

The constructor of the class consists on a single class member assignment:

/*
 * Case passed on class creation (the actual ticket from the Trigger)
 */
private Case ticket{get;Set;}

/*
 * Constructor
 */
public CaseQueuebleJob(Case ticket){
    this.ticket = ticket;
}

Finally, let’s watch the main execute method of the job (the one that stores all the aynchronous logic):

// Interface method. 
// Creates the map of non-null Case fields, gets the Callout__c object
// depending on current context JobID.
// In case of failure, the job is queued again.

public void execute(QueueableContext context) {
    //1 - creates the callout payload
    String reqBody = JSON.serialize(createFromCase(this.ticket));

    //2 - gets the already created Callout__c object
    Callout__c currentCallout = [Select Id, Status__c, Sent_on__c, Response__c, Case__c,
                                 Job_ID__c From Callout__c Where Job_ID__c = :context.getJobId()];

    //3 - starting time (to get Duration__c)
    Long start = System.now().getTime();

    //4 - tries to make the REST call
    try{
        Http h = new Http();
        HttpRequest request = new HttpRequest();
        request.setMethod('POST');
        //change this to another bin @ http://requestb.in
        request.setEndpoint('http://requestb.in/nigam7ni');
        request.setTimeout(60000);
        request.setBody(reqBody);
        HttpResponse response = h.send(request);

        //4a - Response OK
        if(response.getStatusCode() == 200){
            currentCallout.status__c = 'OK';
        //4b - Reponse KO
        }else{
            currentCallout.status__c = 'KO';
        }
        //4c - saves the response body
        currentCallout.Response__c = response.getBody();
    }catch(Exception e){
        //5 - callout failed (e.g. timeout)
        currentCallout.status__c = 'Failed';
        currentCallout.Response__c = e.getStackTraceString().replace('\n',' / ')+' - '+e.getMessage();

        //6 - it would have been cool to reschedule the job again 🙁
        /*
         * Apprently this cannot be done due to "Maximum callout depth has been reached." exception
        ID jobID = System.enqueueJob(new CaseQueuebleJob(this.ticket));
        Callout__c retry = new Callout__c(Job_ID__c = jobID, 
                                             Case__c = this.ticket.Id,
                                            Status__c = 'Queued');
        insert retry;
        */
    }
    //7 - sets various info about the job
    currentCallout.Sent_on__c = System.now();
    currentCallout.Duration__c = system.now().getTime()-start;
    update currentCallout;
    //8 - created an Attachment with the request sent (it could be used to manually send it again with a bonification tool)
    Attachment att = new Attachment(Name = 'request.json', 
                                    Body = Blob.valueOf(reqBody), 
                                    ContentType='application/json',
                                   ParentId = currentCallout.Id);
    insert att;
}

These are the steps of execution:

1) Creates the JSON payload to be sent though the POST request (watch the method in the provided github repo) for more details (nothing more than a describe and a map).

2) Gets the Callout__c SObject that was created by the Case trigger (and using the context’s Job ID).

3) Gets the starting time of the callout being executed (to calculate the duration).

4) Tries to make the rest call

     a. Server responded with a 200 OK

     b. Server responded with a non OK status (e.g. 400, 500)

     c. Saves the response body in the Response__c field

 5) Callout failed, so the Respose__c field is filled with the stacktrace of the exception (believe me this is super usefull when trying to get what happened, expecially when you have other triggers / code in the “try” branch of the code).

6) Unfortunately, if you try to enqueue another job after a callout is done, you get the “Maximum callout depth has been reached.” exception; this is because you can have only two jobs in the queue chain that makes callouts, so if you queue another job with the Database.AllowsCallouts interface, you get this error. This way the job would have tried to enqueue another equal job for future execution.

7) Sets time fields on the Callout__c object.

8)Finally, creates an Attachment object with the JSON request done: this way it can be expected, knowing the precise state of the Case object sent, and can be re-submitted using a re-submission tool that uses the same code (it could be a Batch job for instance).

  This is an example request (if you are curious about what I’m sending):

And this is an example request:
{
    "values": {
        "lastmodifiedbyid": "005w0000003fj35AAA",
        "businesshoursid": "01mw00000009wh7AAA",
        "casenumber": "00001001",
        "ownerid": "005w0000003fj35AAA",
        "createddate": "2015-01-20T09:54:17.000Z",
        "origin": "Phone",
        "isescalated": false,
        "status": "Closed",
        "accountid": "001w0000019wqEIAAY",
        "systemmodstamp": "2015-01-20T19:33:31.000Z",
        "isdeleted": false,
        "priority": "High",
        "id": "500w000000fqNRaAAM",
        "lastmodifieddate": "2015-01-20T19:33:31.000Z",
        "isclosedoncreate": true,
        "createdbyid": "005w0000003fj35AAA",
        "contactid": "003w000001EetwEAAR",
        "type": "Electrical",
        "closeddate": "2015-01-20T19:19:51.000Z",
        "subject": "Test queueable interface",
        "reason": "Performance",
        "potentialliability": "Yes",
        "isclosed": true
    }

As already written, the full code for this Queueable Apex use case, with the related metadata, is available on this GitHub repo.


About the author

Enrico Murru (enree.co) is a Solution Architect and Senior Developer at WebResults (Engineering Group).

He started working on the Force.com platform in 2009 and since then he grew a big experience following the growth of the platform; he is also a huge Javascript lover.
In the last years he began to write technical blog posts for the dev community trying to document his fun with the Force.com platform and more.
His daydream is to become the first italian Force.com evalgelist, sharing his passion to all developers, and spend the rest of his professional life (and more) to learn new techs and experimenting.

Published
May 13, 2015
Topics: