Move your callouts before your DML statements.
In other words make sure that all update
, insert
and delete
statements in your execute
method occur after any callout statements. A quick search of "uncommitted work" will reveal that there are plenty of questions on this site that address this issue
Here's an example :
Assume you have a queue of "contacts" off accounts, each contact has a url (Url__c)
and a state (State__c)
. State would be set to EMAIL-PENDING just before you make your callout
This would be a problem and fire the uncommited
error :
public void execute( ... )
{
for(Sobject s : contacts)
{
contact.put('State__c', 'EMAIL-PENDING');
update contact;
// http callout (contact.Url__c)
// send-email( contact.emailAddress )'
contact.put('State__c', 'EMAIL-SENT');
update contact;
}
}
Possible solution : modify your logic so that you focus on state showing the emails actually sent ( EMAIL-SENT )
public void execute( Database.BatchableContext BC, List<SObject> contacts )
{
for(Sobject s : contacts)
{
// http callout (contact.Url__c)
// send-email( contact.emailAddress )'
contact.put('State__c', 'EMAIL-SENT');
update contact;
}
}
Alternative solution, move from a query locator
to an iterable
in your batch and use the start
context to set all work as EMAIL-PENDING. This approach allows you to perform state changes / DML before you make the callout
global Iterable<...> start(Database.BatchableContext info) {
for(Sobject s : contacts)
{
contact.put('State__c', 'EMAIL-PENDING');
update contact;
}
return....
}
public void execute( Database.BatchableContext BC, List<SObject> contacts )
{
for(Sobject s : contacts)
{
// http callout (contact.Url__c)
// send-email( contact.emailAddress )'
}
for(Sobject s : contacts)
{
contact.put('State__c', 'EMAIL-SENT');
update contact;
}
}
Definitely you can use an HTTP callout as an escape mechanism. But this is pretty naughty. You'll need a Remote Site Setting and a Session ID and a global class etc etc etc (red flags, alarm bells).
May I humbly recommend the use of System.Queueable
call instead of @Future
. You can create a class that is constructed with any kind of argument that you want to hand in, like so:
public class DeferredHandler implements System.Queueable
{
private Map<String,String> key2val;
public DeferredHandler(Map<String,String> key2val) {
this.key2val = key2val;
}
public void execute(System.QueueableContext objContext)
{
//do your @Future stuff here with this.key2val
}
}
And invoke him from inside your batch like this:
//defer stuff
System.Queueable job = new DeferredHandler(key2val);
System.enqueueJob(job);
There is some literature about this in the docs and Josh Kaplan's awesome Queueable blog post.
Best Answer
UPDATED ANSWER
(not batch but...).
I was investigating the ScheduledDispatcher: https://gist.github.com/gbutt/11151983
And lo and behold this works:
If you still need to do it from within the batch context you can do as previously suggested:
call method1 from the batch and method2 from elsewhere