0

I have implemented the batch which is executed from apex controller. Now, if one job of the batch fails (due to some exception), I need to rollback the changes not only for that job but for all the jobs in that batch which have executed.

I tried stateful batch with declaring the Savepoint() globally and initializing it in the execute() method, such that it will be initialized only once. Then I am rolling back from the catch block. But I am getting the run time error as System.SerializationException: Not Serializable: System.Savepoint

Another approach is to delete all the records from the finish() method, but since that object's trigger as some heavy processing in AfterDelete context. So, I might get CPU Time Limit exception.

Please suggest any alternative solution.

3 Answers 3

2

You can't rollback a fully completed transaction, which is why you get a SerializationException if you try to save a SavePoint. The start method, each call to execute, and the finish methods are all separate transactions. For example, with a batch size of 200, if you process 400 records, there are four total transactions: one start, two execute, and one finish method. You would have to run another batch to undo (if possible) whatever you did.

1

Currently you are committing your work in the same transaction in which it is calculated. You don't specify, but I imagine it's something like

start() {
  //zero out the Running Total field in all the records in some Summary-type object
}

execute() {
  //for each record in the querylocator
    //Add a number value in this record to the Running Total field in the Summary object
  //DML update the affected Summary records
}

So, right, if one execute crashes now all your Running Totals or whatever are corrupted.

I suggest that you break the task into two pieces to be done separately. Key to this could be to use a new sObject as a staging area to hold the results of each execute scope. Then in your

finish() {
  //check to make sure all the separate executes completed successfully
  //if they did
    //query for all the Running_Total_Staging__c records created during this batch run
    //query for all the Summary__c records that need updated
    //populate the Running Total field on the Summary records
    //set savepoint
    //update Summary__c records
    //if needed, roll back
  //if one or more of the executes had a problem
    //notify somebody
}

So unless the whole batch is successful, none of the Summary records get updated. Or whatever it is you're doing.

3
  • 1
    one has to be careful to not exceed DML limits in finish() ..e.g. 10,000 recs - might have to launch a batch job to do the updates
    – cropredy
    Commented Apr 23, 2021 at 23:16
  • That's what I thought, to create a staging object. But, it would have mean, creating staging object (with fields, relationships and validations) same as that of original object, which is a very heavy task. So, this was also not applicable solution for me.
    – A m
    Commented May 28, 2021 at 7:22
  • It doesn't have to be a different sObject, technically. You could create a different "Staging" record type or some other field to differentiate, and just create that particular type of record. This might have its own drawbacks, of course. You would need to easily partition these records from the production records in that table. Commented Jun 1, 2021 at 17:13
0

Note that sfdcfox is absolutely right about the fact that earlier DML transactions (from execute) have been fully committed so chunks of output from the batch may have already been saved to the database by the time you have execute fail for some reason.

If your batch is updating existing objects, you're rather stuck unless you have a mechanism to undo your changes (this would likely mean having to do some recording of individual changes, capturing old values etc., will not be robust since undoing of changes could still fail due to validation rules etc. and this would be a huge amount of work). On the other hand, if your batch is only creating new records, consider:

  1. Adding a String field, large enough to hold a batch job ID, to the object(s) your batches create. This will hold the ID of the job used to create the individual record.
  2. Ensuring that your batch implements Database.RaisesPlatformEvents to allow you to react when the batch fails (be that for thrown exceptions or governor limits).
  3. Having your batch set the string field from point 1 to the current batch ID when creating the records it creates.
  4. Providing a trigger-based handler for BatchApexErrorEvent that:
    1. Looks for your specific batch class(es) as the source of the event(s), collating all of such events to generate a Set<String> of the batch job ID(s) for these failures.
    2. Filters these to be only those batch jobs that are still executing.
    3. Uses System.abortJob with these filtered batch job ID(s) to make sure these still executing but faulty batches are terminated. This prevents further records being inserted by the batch(es).
    4. Only then runs a separate batch that takes the unfiltered list of batch job ID(s) that simply finds all your records where the string field from 1 matches any of these listed failed batches and deletes them. Note that this batch needs to use Database.delete with allOrNone = false to avoid issues with a record being deleted before this batch gets to it, and to handle other failures for the deletion.

Again, this could fail because of logic explicitly triggered on deletion, but seems about the most robust way to handle it from what I can see.

You must log in to answer this question.

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