Interesting question with some interesting answers. I think/hope I've understood what you're asking. In some respects, this feels a bit like fighting the platform here, and I guess it'll depend on how you feel about doing callouts back into the same instance of Salesforce using UserInfo.getSessionId()
, but I think you could do something using this approach to give what you need.
To illustrate, I have 2,810 Contacts in my org where I'm simply updating a custom field value Val__c
to a numeric value (just so i can see the results each time).
I have a batch apex class which I'm calling with a chunk size of 500 so that I get 6 iterations. In one of those iterations (#4), I'm going to deliberately throw an Exception (i.e. one that can be caught) and then in iteration #5 I'm going to create a Limits Exception (i.e. one that cannot be caught) and all the others will succeed.
At the end of the batch, I should have updated 1,810 records and 1,000 will remain untouched/rolled back thanks to the platform rollback feature (all good). I'm going to preserve state across each batch execution and report the errors in the finish
method.
Batch Apex Class
This doesn't do a whole lot of work, it basically makes the callouts (back to SF) which have their own execution context, so essentially each iteration is isolated (and will be committed/rolled back in isolation), except for the state I'm passing between them. It is also possible to retrieve the error message from the REST worker class, even if we don't catch the error in that class (see makeDMLCallout
)
global class MainBatch implements Database.Batchable<sObject>, Database.Stateful, Database.AllowsCallouts
{
private String sessionId;
private Decimal val;
private Integer iteration = 0;
private List<String> errors = new List<String>();
global MainBatch(Decimal val, String sessionId)
{
this.val = val;
this.sessionId = sessionId;
}
global Database.QueryLocator start(Database.BatchableContext BC)
{
return Database.getQueryLocator([select Val__c From Contact]);
}
global void execute(Database.BatchableContext BC, List<SObject> scope)
{
// create some state to pass to REST service
BatchData bd = new BatchData();
bd.val = this.val;
bd.recordIds = new Map<Id,SObject>(scope).keySet();
bd.iteration = this.iteration++;
// make first callout
makeBenignCallout(bd);
// make second one
makeDMLCallout(bd);
}
global void finish(Database.BatchableContext BC)
{
System.debug('Iteration = ' + this.iteration);
System.debug('Errors='+JSON.serializePretty(errors));
}
global class BatchData
{
public String sessionId {get;set;}
public Decimal Val {get;set;}
public Set<Id> recordIds {get;set;}
public Integer iteration {get;set;}
}
private void makeDMLCallout(BatchData bd)
{
String addr = URL.getSalesforceBaseUrl().toExternalForm() + '/services/apexrest/RestWorker';
HttpRequest req = new HttpRequest();
req.setTimeout(60000);
req.setEndpoint( addr );
req.setMethod('POST');
req.setHeader('Authorization', 'OAuth ' + bd.sessionId);
req.setHeader('Content-Type','application/json; charset=UTF-8');
req.setHeader('Accept','application/json');
req.setHeader('Accept-Encoding','gzip');
req.setBodyAsBlob( Blob.valueOf( JSON.serialize(bd) ) );
Http http = new Http();
HttpResponse response = http.send(req);
String body = response.getBody();
if(response.getStatus()=='OK')
{
// celebrate
}
else
{
List<Object> results = (List<Object>)JSON.deserializeUntyped(body);
Map<String,Object> result = (Map<String,Object>)results[0];
String message = (String)result.get('message');
errors.add('Preserved Error Message: '+message);
}
}
private static void makeBenignCallout(MainBatch.BatchData bd)
{
String addr = URL.getSalesforceBaseUrl().toExternalForm() + '/services/apexrest/RestWorker';
HttpRequest req = new HttpRequest();
req.setTimeout(60000);
req.setEndpoint( addr );
req.setMethod('GET');
req.setHeader('Authorization', 'OAuth ' + bd.sessionId);
req.setHeader('Content-Type','application/json; charset=UTF-8');
req.setHeader('Accept','application/json');
req.setHeader('Accept-Encoding','gzip');
Http http = new Http();
HttpResponse response = http.send(req);
}
}
REST service class
This is basically a faux implementation of the batch apex's execute
method, called via the REST api. The platform is kind enough to return us a HttpResponse which includes the error message, whether it be a catchable, or non-catchable exception + rolling back etc. as it should:
@RestResource(urlMapping='/RestWorker')
global class RestWorker
{
@HttpGet
global static void doBenignUnitOfWork()
{
System.debug('nothing to see here');
}
@HttpPost
global static WorkerResponse doUnitOfWork()
{
// deserialize the JSON passed from MainBatch
String jsonString = System.RestContext.request.requestBody.toString();
MainBatch.BatchData bd = (MainBatch.BatchData)JSON.deserialize(jsonString, MainBatch.BatchData.class);
// query the dataset based on the record id's in the batch
List<Contact> contacts = [Select Val__c From Contact Where Id IN :bd.recordIds];
// do something
for(Contact contact : contacts)
{
contact.Val__c = bd.Val;
}
update contacts;
// simulate a failure we can control
if(bd.iteration == 3)
{
throw new DeliberateException('Deliberate Exception Thrown in Iteration ' + bd.iteration);
}
// similate a limits exception
if(bd.iteration == 4)
{
causeLimitsException();
}
return new WorkerResponse();
}
private static void causeLimitsException()
{
for(Integer i = 0 ; i <= 100 ; i++)
{
List<Contact> contacts = [Select Val__c From Contact limit 1];
}
}
global class WorkerResponse
{
public String result {get;set;}
}
public class DeliberateException extends Exception{}
}
The results
After running this test against my 2,810 i get the expected results:
- 1,810 Contacts updated with the correct numeric value
- 1,000 Contacts rolled back by platform
- 2 x Error messages (one
RestWorker.DeliberateException
and one System.LimitException: Too many SOQL queries: 101
exception) output in the finish
method
- iteration value of 6
Obviously, needs some work but could potentially be made to work unless i've missed something obvious.
Database.Stateful
IMHO. I'll have a tonk and see what happens to the in-memory properties during automatic system rollback.