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);
}
}
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 multipleprocessor_impl__c
returned in the SOQL, you could order byprocessor_impl__c
or save in a map of lists before sending to response processor. There are also many error handling considerations in theCalloutResolvedTrigger