3

Can a long-running Batch use a Custom Setting's value as an indicator as to whether or not it should continue to process records?

I have a batch that will process a few thousand records that could run for a few hours and I would like it to check a Custom Setting value to see if it should keep processing. I would like to modify the Custom Setting's value manually to halt the Batch when required.

My understanding of Custom Settings is that all of the values are kept in the application cache so I am concerned that the value the Batch retrieves could be stale after I have updated it manually. That said, my execute() method only processes one record at a time so it would make sense that the each run of execute() would get a refreshed set of Custom Settings.

(Note: it is my execute() method that will check for this flag. If the flag is false, it will not process the single record that was passed to it. This means all remaining items in the list will be quickly run through without any processing occurring which is quite acceptable.)

3

I just tested this, there is a caveat, but it does work.

First, some code:

global class LongRunningBatch implements Database.Stateful, Database.Batchable<Account>, Iterable<Account>, Iterator<Account> {
    Integer subcounter = 0, counter = 0, max = 0;

    global LongRunningBatch(Integer total) {
        max = total;
    }

    global Iterable<Account> start(Database.BatchableContext bc) {
        return this;
    }
    global Iterator<Account> iterator() {
        return this;
    }
    global Boolean hasNext() {
        return counter < max;
    }
    global Account next() {
        return new Account(Name=String.valueOf(++counter));
    }
    global void execute(Database.BatchableContext bc, Account[] records) {
        if(Settings__c.getInstance('batch')!=null && Settings__c.getInstance('batch').Value__c == 'STOP') {
            // counter = max;
            // Above didn't stop my batch. *sigh*
            // But at least it returns early.
            return;
        }
        SavePoint sp = Database.setSavePoint();
        try {
            insert records;
            delete records;
        } catch(exception e) {

        }
        Database.rollback(sp);
        subcounter = counter;
    }
    global void finish(Database.BatchableContext bc) {
        Messaging.SingleEmailMessage message = new Messaging.SingleEmailMessage();
        message.setSubject('Finished processing');
        message.setPlainTextBody('Finished processing with '+String.valueof(subcounter)+' records processed, '+string.valueof(counter)+' records queued.');
        message.setTargetObjectId(UserInfo.getUserId());
        Messaging.sendEmail(new Messaging.SingleEmailMessage[] { message });
    }
}

Let me explain the original rationale:

I thought the system scheduler code did something like this:

SObject[] records = new SObject[0];
while(iterator.hasNext()) {
   records.add(iterator.next());
   if(records.size()==scope) {
      execute(records);
      records.clear();
   }
}

Unfortunately, it does not. It does something more like this:

SObject[] allrecords = new SObject[0], thisBatch = new SObject[0];
while(iterator.hasNext()) {
  allRecords.add(iterator.next());
}
while(!allRecords.isEmpty()) {
  thisBatch.add(allrecords.remove(0));
  if(thisbatch.size()==scope) {
    execute(context, thisbatch);
    thisbatch.clear();
  }
}

This is obviously an over-simplification, but for the purposes of what's going on here, it defines the logic we're working with. The design means that there's a practical upper limit to how many items you can process with an Iterable class (I believe it caps out around 500,000 for simple iterators).

At any rate, given this in mind, I kicked off the code with the following statement:

LongRunningBatch b = new LongRunningBatch(250000);
Database.executeBatch(b);

Here's a profiling log without the setting in place:

08:57:24:000 CUMULATIVE_PROFILING External entry point: global void execute(Database.BatchableContext, LIST<Account>): executed 1 time in 1302 ms

Here's a profiling log with the setting in place:

08:57:26:000 CUMULATIVE_PROFILING External entry point: global void execute(Database.BatchableContext, LIST<Account>): executed 1 time in 14 ms

The logs also indicate that no DML actions occurred while the batch setting was configured. In my Developer Console window, you can see it take place immediately:

Batch Apex 06:57:24 ...
/setup/ui/editCustomSettingsData 06:57:25 ...
Batch Apex 06:56:26 ...

Those debug lines from above are the from the log files immediately before and after the change in custom settings. The effect was visible in a single batch.

That being said, the batch can't fully abort itself early, but in the event of a long running batch, it can reduce its execution time significantly. You should note that there appears to be a significant delay between batch executions (because it is a shared resource), so the batch may only finish marginally faster if it is processing a large number of small transactions, as opposed to large transactions (e.g. if your execution time is less than 10k lines of code and/or 2 seconds of execution, there's probably no reason to implement a kill-switch like this).

  • That's exactly what I was hoping for. My batch makes service calls and then updates records accordingly so the kill-switch is necessary for business reasons more than timing reasons but thanks for the clarification. – Haphazard Jul 30 '13 at 14:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.