6

I just came up with the following pattern for making callouts. It's loosely based on Dan Appleman's Scheduler pattern.

Has anyone out there has used or dismissed this? Why did you dismiss it? How did it work if you used it? Is this such a bad idea that no one's thought of it?

My (pretty common) problem:

When an address is entered or changes, I want to re-geocode it (using Google's geocoding APIs).

Platform-agnostic analysis

In general, I plan to geocode addresses in JavaScript as they are typed in the UI.

However, I expect that at some point, people (ok, probably me) will load addresses in devious ways that avoid that UI. I obviously want to re-geocode that, too.

To track which records need geocoding, I will add a field, Has_Been_Geocoded__c to the address-containing table, start it off false and if the address changes, I will make it false again.

Then, all I need is a process that looks at addresses where Has_Been_Geocoded__c = FALSE and geocodes them.

Salesforce constraints

That process cannot be scheduled because those can only run once an hour (which is too slow if Google already rate-limits me to 10 calls at a time). Also, I cannot call an external web service directly from a Trigger.

But I already have a nice testable and generalizable implementation that I like that involves a GeocodingData class, and can be reused across other Objects (s- and otherwise) without much work...

So why not do this?

Add a CustomObject called CalloutData__c, and a generic CalloutExecutor class. That CalloutExecutor class looks something like this:

public class CalloutExecutor implements Queueable, Database.AllowsCallouts {

  public void execute(QueueableContext context) {
    Integer queryLimit = // the lesser of remaining Callouts and remaining DML statements
    List<CalloutData__c> cds = new List<CalloutData__c>();
    for (CalloutData__c cd : [SELECT Id, Endpoint__c, Payload__c, Method__c, 
                                     Req_Headers__c, Is_Processed__c,
                                     Rsp__c, Rsp_Headers__c, Status__c, 
                                     Errors__c, JobId__c
                              FROM CalloutData__c
                              WHERE Is_Processed__c = false
                              LIMIT :queryLimit]) {
      cd.JobId__c = context.getJobId();
      try {
        HttpRequest req = generateRequest(cd);
        HttpResponse rsp = callout(req);
        setResponseData(cd, rsp);
      } catch (Exception e) {
        cd.Errors__c = e.toString();
      }
    }
    update cds;
  }

  private HttpRequest generateRequest(CalloutData__c cd) {
    // ...
  }

  private HttpResponse callout(HttpRequest req) {
    return Http.new().send(req);
  }

  private void setResponseData(CalloutData__c cd, HttpResponse rsp) {
    cd.Rsp_Headers__c = rsp.getHeaders();
    cd.Rsp__c = rsp.getBody();
    cd.Status__c = rsp.getStatus();
    return cd;
  }
}

Then, there's a trigger on CalloutData__c that does something like this:

trigger CalloutResolvedTrigger on CalloutData__c (after update) {

  List<Id> ids = new List<Id>(Trigger.newMap.keySet());
  for (CalloutData__c cd : [SELECT ProcessorImpl__c, Rsp__c,
                                   Rsp_Headers__c, Status__c, 
                                   Errors__c
                            FROM CalloutData__c
                            WHERE Is_Processed__c = true
                            AND Id IN :ids]) {
    CalloutResponseProcessor processor = // instanciate ProcessorImpl__c class
    processor.process(Rsp__c, Rsp_Headers__c, Status__c, Errors__c);    
  }
}
1
  • I'd probably implement processor.process to work on lists as otherwise you run risk of soqls inside of for loop in the CalloutResolvedTrigger (I recognize that there can be multiple processor_impl__c returned in the SOQL, you could order by processor_impl__c or save in a map of lists before sending to response processor. There are also many error handling considerations in the CalloutResolvedTrigger
    – cropredy
    Commented Jan 8, 2016 at 1:11

1 Answer 1

2

One problem with using Queueable is that you can't callout and chain to more Queueable calls. Generally speaking, a far better solution is to simply have a scheduler that runs hourly, then have it call a Batchable class that performs the same type of logic. You can chain this Batchable class as much as you'd like (within Google's rate limiting). This allows you to go as fast as Google permits. Most of your code is already acceptable as-is, you just need to build a start and finish method.

Edit:

Calling out in a batch process is trivial:

public class Geolocator implements Database.Batchable<CalloutData__c>, Database.AllowsCallouts {
    public QueryLocator start(Database.BatchableContext context) {
        return Database.getQueryLocator([SELECT Id, Endpoint__c, Payload__c, Method__c, 
                                     Req_Headers__c, Is_Processed__c,
                                     Rsp__c, Rsp_Headers__c, Status__c, 
                                     Errors__c, JobId__c
                              FROM CalloutData__c
                              WHERE Is_Processed__c = false]);
    }
    public void execute(Database.BatchableContext context, CalloutData__c[] records) {
        // Do callouts here, update records
    }
    public void finish(Database.BatchableContext context) {
        // Nothing to do here
    }
}
4
  • How would that chaining work? Commented Jan 8, 2016 at 2:59
  • @CharlesKoppelman The "chaining" is taken care of for you. I've taken the liberty of drafting up a simple starter for you.
    – sfdcfox
    Commented Jan 8, 2016 at 3:07
  • I understand how Batchables work, generally. I'm confused what you mean by chaining here, in, "You can chain this Batchable class as much as you'd like (within Google's rate limiting)." How do I do more than 10/hour? Commented Jan 8, 2016 at 15:48
  • @CharlesKoppelman The scheduler would only run hourly. However, batches can run against hundreds, thousands, even millions of records at once. Realistically, you should probably just schedule it daily and have your batch process up to the maximum daily limit once per day. Even if you're a premium user, you'd only have 100,000 records you could do per day, and salesforce can run through that many callouts probably at about 200 per second (you'll need to artificially slow down your code to Google's rate limits). If you average it over time, you're allowed one geocode per about 1.2 seconds.
    – sfdcfox
    Commented Jan 8, 2016 at 21:14

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .