[SalesForce] Why is calling abortJob() from within Batch’s execute() not working

I have a batch process that I'd like to completely halt if an exception is encountered. According to the Apex docs on Using Batch Apex, Database.BatchableContext.getJobID:

Returns the ID of the AsyncApexJob object associated with this batch job as a string. Use this method to track the progress of records in the batch job. You can also use this ID with the System.abortJob method.

I've setup my execute method as follows:

global void execute(Database.BatchableContext BC, List<sObject> scope) {
  try {
    // do some work, maybe throw exceptions
  }
  catch (Exception e) {
    system.debug('Exception in DataMigration_Batch.execute(): ' + e);
    system.debug(e.getStackTraceString());

    // cancel import
    string importJobId = BC.getJobId(); // docs state this is string;
                                        // also tried declaring as id
    system.debug('importJobId: ' + importJobId);
    System.abortJob(importJobId);

    throw new DataMigrationException(e.getMessage());
  }
}

If I encounter an exception in the first batch, I see the debug messages, and the new exception is thrown at then and of the catch block. But the Batch job is NOT cancelled. In developer console I see each batch's log, and I see a log for the finish() method (to which I've added debugging output to be certain it's running). In the Apex Jobs setup UI page, I see the job in completed status with all batches enumerated.

When starting the job I log the Job ID, and I've compared the value to the value logged in my exception handler which is then passed to abortJob(). They are the same.

I have seen multiple posts online (e.g., SF Developer Forums, this SFSE post) that show abortJob() being called from within execute(), and I can find nothing in the docs to suggest that it should not work. But I've run a job with 39 failing batches, each of which called abortJob(), and still the job ran to the finish() method, and did not abort.

Note that while it is my understanding that abortJob() should prevent finish() from running, I don't care if finish() runs or not; I just want to stop running execute() once an individual batch fails.

Best Answer

If you throw an exception, and it's not caught, you abort the entire transaction, rolling the database back to the state it was in before the execute method started. This means that you've effectively aborted your abort call by rolling it back. You can't show a failed transaction via a thrown exception and cause your code to abort, at least not directly. You could create a Platform Event, and fire off the abort call in the Platform Event's trigger handler. Since Platform Events cannot be rolled back, this would have the intended side effect of killing the job early. Edit: Platform Events do successfully abort the transaction early.


Demo code:

public class JobKillerDemo implements Database.Batchable<Integer> {
    public Integer[] start(Database.BatchableContext context) {
        Integer[] items = new Integer[0];
        while(items.size() < 1000) items.add(items.size());
        return items;
    }
    public void execute(Database.BatchableContext context, Integer[] scope) {
        if(scope[0] == 3) {
            EventBus.publish(new JobKiller__e(JobId__c=context.getJobId()));
            Integer i = 0 / 0;
        }
    }
    public void finish(Database.BatchableContext context) {

    }
}

trigger JobKiller on JobKiller__e (after insert) {
    for(JobKiller__e job: Trigger.new) {
        System.abortJob(job.JobId__c);
    }
}

Execute with a batch size of 1.

Note that since Platform Events are processed asynchronously, some additional batches may process in the interim; in a test, this batch process went through 3 additional batches (7 total batches) before the abort took effect. This still prevented 993 executions, so that's definitely an improvement.

Related Topic