3

I have a CommandButton that calls a method in the controller that starts an Apex batch processes. I have an ActionPoller that is supposed to get the current progress of the batch process and update the page every 5 seconds. However, it doesn't seem to start its 5-second timer until after the batch process is completely finished. Isn't the batch process supposed to be asynchronous? How can I fix this?

Here is some of my code...

Visualforce:

<apex:form >
    <apex:outputLabel id="mainSummary" style="font-size:12pt">
        We found <b>{!numTotal}</b> total* US addresses
    </apex:outputLabel>&nbsp;
    <apex:commandButton id="verifyAllBtn" value="Submit All Addresses" action="{!verifyAll}"/>&nbsp;
    <apex:outputText id="verifyAllStatus" value="{!verAllStatus}"/>

...

<apex:actionPoller id="poller" action="{!getStatus}" interval="5" rendered="{!poll}"
          reRender="verifyAllStatus,verifyNewStatus,verifyInvalidStatus,verifyValidStatus,mainSummaryBreakdown"/>

</apex:form>

Controller:

public PageReference verifyAll(){
    currStatusType = 'All';
    batchVer.addresses.clear();
    batchVer.addresses.addAll(batchVer.newAddresses);
    batchVer.addresses.addAll(batchVer.invalid);
    batchVer.addresses.addAll(batchVer.clean);

    batchVer.verifyAddresses();
    verAllStatus = showResults();

    return null;
}

private string showResults(){        
    poll = true;

    return 'Verification complete: '+batchVer.newClean.size()+' valid, '+
        batchVer.newInvalid.size()+' invalid. Updating records... 0% completed';
}

public PageReference getStatus() {
    AsyncApexJob batchJob;
    string status = 'Processing... '+batchVer.addrIndex+'/'+batchVer.addresses.size()+' completed';
    System.debug('Verification progress: ' + batchVer.addrIndex+'/'+batchVer.addresses.size());

    if (batchVer.currBatchId != null){
        batchJob = [SELECT Id, Status, JobItemsProcessed, TotalJobItems, NumberOfErrors
                FROM AsyncApexJob WHERE ID =: batchVer.currBatchId ];

        if (batchJob != null){
            System.debug('Update progress: '+(batchJob.JobItemsProcessed/batchJob.TotalJobItems)*100+'% completed');
            status = 'Verification complete: '+batchVer.newClean.size()+' valid, '+
                batchVer.newInvalid.size()+' invalid. ';
            if (batchJob.JobItemsProcessed == batchJob.TotalJobItems)
                status += 'Records updated.';
            else{
                status += 'Updating records... (may take a few minutes)';

            }

        }
    }

    if (currStatusType == 'All')
        verAllStatus = status;
    else if (currStatusType == 'New')
        verNewStatus = status;
    else if (currStatusType == 'Invalid')
        verInvalidStatus = status;
    else if (currStatusType == 'Valid')
        verValidStatus = status;

    if (batchJob != null && batchJob.JobItemsProcessed == batchJob.TotalJobItems){ // All done
        currStatusType = 'None';
        poll = false;
    }

    return null;
}

batchVer is an instance of BatchVerifier, the Apex Class that does the heavy lifting. The method verifyAddresses() is public, and eventually calls updateRecords().

BatchVerifier:

private void updateRecords(){
    List<sObject> records = new List<sObject>();
    for (SFAddress address : this.addresses) {
        if (address.type == 'Billing' || address.type == 'Shipping'){ // Account address
            Account account = accounts.get(address.id);
            if (account != null){
                if (address.type == 'Billing'){
                    account.BillingStreet = address.street;
                    account.BillingCity = address.city;
                    account.BillingState = address.state;
                    account.BillingPostalCode = address.zip;
                    account.SmartyStreets1__BillingVerified__c = address.verified;
                    account.SmartyStreets1__BillingSummary__c = address.summary;
                    if (address.verified)
                        account.SmartyStreets1__BillingVerDate__c = Date.today().format();
                    else account.SmartyStreets1__BillingVerDate__c = '---';
                }
                else if (address.type == 'Shipping'){
                    account.ShippingStreet = address.street;
                    account.ShippingCity = address.city;
                    account.ShippingState = address.state;
                    account.ShippingPostalCode = address.zip;
                    account.SmartyStreets1__ShippingVerified__c = address.verified;
                    account.SmartyStreets1__ShippingSummary__c = address.summary;
                    if (address.verified)
                        account.SmartyStreets1__ShippingVerDate__c = Date.today().format();
                    else account.SmartyStreets1__ShippingVerDate__c = '---';
                }                
            }
        }

        if (address.type == 'Mailing' || address.type == 'Other'){
            Contact contact = contacts.get(address.id);
            if (contact != null){
                if (address.type == 'Mailing'){
                    contact.MailingStreet = address.street;
                    contact.MailingCity = address.city;
                    contact.MailingState = address.state;
                    contact.MailingPostalCode = address.zip;
                    contact.SmartyStreets1__MailingVerified__c = address.verified;
                    contact.SmartyStreets1__MailingSummary__c = address.summary;
                    if (address.verified)
                        contact.SmartyStreets1__MailingVerDate__c = Date.today().format();
                    else contact.SmartyStreets1__MailingVerDate__c = '---';
                }
                else if (address.type == 'Other'){
                    contact.OtherStreet = address.street;
                    contact.OtherCity = address.city;
                    contact.OtherState = address.state;
                    contact.OtherPostalCode = address.zip;
                    contact.SmartyStreets1__OtherVerified__c = address.verified;
                    contact.SmartyStreets1__OtherSummary__c = address.summary;
                    if (address.verified)
                        contact.SmartyStreets1__OtherVerDate__c = Date.today().format();
                    else contact.SmartyStreets1__OtherVerDate__c = '---';
                }
            }    
        }

        if (address.type == 'Lead'){
            Lead lead = leads.get(address.Id);           
            if (lead != null){
                lead.Street = address.street;
                lead.City = address.city;
                lead.State = address.state;
                lead.PostalCode = address.zip;
                lead.SmartyStreets1__Verified__c = address.verified;
                lead.SmartyStreets1__Summary__c = address.summary;
                if (address.verified)
                    lead.SmartyStreets1__VerDate__c = Date.today().format();
                else lead.SmartyStreets1__VerDate__c = '---';
            }    
        }
    }

    records.addAll((List<sObject>)accounts.values());
    records.addAll((List<sObject>)contacts.values());
    records.addAll((List<sObject>)leads.values());

    try{
        Batcher batcher = new Batcher();
        batcher.records = records;
        if (Test.isRunningTest() && batchCrashTest)
            throw new TestException('Test Crash');
        currBatchId = Database.executeBatch(batcher);
    }
    catch(Exception e) {
        System.debug('Error executing batch: '+e);
    }
}

Batcher:

global class Batcher implements Database.Batchable<sObject>{
    List<BatchVerifier.SFAddress> addresses;
    public List<sObject> records;


    global List<sObject> start(Database.BatchableContext info){ 
        return records;
    }

    global void execute(Database.BatchableContext info, List<sObject> scope){
        if (scope.size() > 0)
            update scope;
    }

    global void finish(Database.BatchableContext info){
    }
}
3
  • Neo, as you've described your situation, I suspect there's something wrong with your code; I wrote code that does it correctly just last week. Can you provide a short, self-contained, correct example of your code that demonstrates your problem?
    – sfdcfox
    Nov 10 '15 at 23:26
  • I've added some example code, @sfdcfox. I hope it's helpful.
    – Neo
    Nov 10 '15 at 23:59
  • You have a couple of silly mistakes in your code. They're easy enough to make, though. Go ahead and check out my answer.
    – sfdcfox
    Nov 11 '15 at 0:07
3

You have two potential problems with your code (as copy-pasted). First, verifyAll doesn't set poll to true, so the actionPoller doesn't know it should be running. Secondly, your commandButton doesn't reRender the poller, so it doesn't get the Boolean updated in its rendered attribute. I've written up an example that works as you'd expect as a gist. Here's the code I wrote for this question, which is a SSCCE:

public class ActionPollerExample implements Database.Batchable<Integer>, Iterator<Integer>, Iterable<Integer> {
    Integer counter;
    public Id jobId { get; set; }
    public Boolean keepPolling { get; set; }

    public ActionPollerExample() {
        counter = 0;
        keepPolling = false;
    }
    public Iterator<Integer> iterator() {
        return this;
    }
    public Integer next() {
        return counter++;
    }
    public Boolean hasNext() {
        return counter<20000;
    }
    public Iterable<Integer> start(Database.BatchableContext context) {
        return this;
    }
    public void execute(Database.BatchableContext context, Integer[] scope) {

    }
    public void finish(Database.BatchableContext context) {

    }
    public void actionPollAction() {
        AsyncApexJob job = [SELECT JobItemsProcessed, TotalJobItems, Status FROM AsyncApexJob WHERE Id = :jobId];
        if(job.Status == 'Queued' || job.Status == 'Holding' || job.Status == 'Preparing') {
            ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.INFO, 'We are in the '+job.Status.toLowerCase()+' status...'));
        } else if(job.Status != 'Completed' && job.Status != 'Aborted') {
            ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.INFO, 'We are in '+job.Status.toLowerCase()+' status: '+Math.floor(100.0*job.JobItemsProcessed/job.TotalJobItems)+'%...'));
        } else {
            ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.CONFIRM, 'The job is now '+job.Status.toLowerCase()+'.'));
            keepPolling = false;
        }
    }
    public void startAction() {
        jobId = Database.executeBatch(this);
        ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.INFO, 'We have started executing the job...'));
        keepPolling = true;
    }
    public void showInitMessage() {
        ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.INFO, 'We will be starting the batch process in five seconds...'));
    }
}

<apex:page controller="ActionPollerExample" action="{!showInitMessage}">
    <apex:form id="form">
        <apex:pageMessages />
        <apex:actionPoller rendered="{!jobId == null}" action="{!startAction}" interval="5" reRender="form" />
        <apex:actionPoller rendered="{!keepPolling}" action="{!actionPollAction}" interval="5" reRender="form" />
    </apex:form>
</apex:page>

Edit: Also, I noticed that your code has an additional problem: it assumes that batchVer is the same instance of the job that's executing. That's not true. It becomes a fully independent version; you need to query the AsyncApexJob record, and get the progress as I've outlined in my example code.

3
  • Thanks for your answer @sfdcfox. I forgot to include showResults(), which sets poll to true. I wondered if batchVer wasn't the same instance; that helps.
    – Neo
    Nov 11 '15 at 20:41
  • I'm still pretty confused, actually. Does actionPoller create a new instance of the controller every time it polls, and call the action on the new instance? Is it important to have the controller implement Database.Batchable (as opposed to having another class be), or did you just do it that way to be concise?
    – Neo
    Nov 11 '15 at 22:25
  • I wrote one class instead of four to be concise, they need not all be the same class. System.executeBatch causes a second instance of this class to spring into existence. actionPoller acts on the first instance of the class (the one that controls the page's business logic).
    – sfdcfox
    Nov 12 '15 at 0:53

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.