[SalesForce] How to get around Apex CPU time limit error when reassigning lead owner via Apex

With the following code, I'm getting an Apex CPU time limit exceeded error:

List<Lead> untouchedLeads = [SELECT id 
                                    FROM Lead 
                                    WHERE Days_Since_Last_Touch__c > 2
                                        AND CreatedDate = LAST_N_WEEKS:6
                                        AND Status = 'Attempting'
                                        AND Owner.Type = 'User'
                                        ORDER BY CreatedDate DESC NULLS LAST LIMIT 508];
    if(untouchedLeads.size() > 0) { 
        // find ID of distribution queue
            List<Group> distQueues = [SELECT Id
                                        FROM Group 
                                        WHERE DeveloperName = 'Distribution_Queue'
                                        AND Type = 'Queue'
                                        ORDER BY CreatedDate DESC NULLS LAST LIMIT 1];
            if(distQueues.size() > 0) {
                //assign untouched leads to distribution queue
                Id distQueueId = distQueues[0].id;
                for(Lead untouchedLead : untouchedLeads){
                    untouchedLead.Ownerid = distQueueId;
            }

       update untouchedLeads;
  }
}

Should I not be updating a list like that?

Here is our only update lead trigger code that's not part of a managed package:

/******************/
/* after update */
/******************/
if (trigger.isAfter && trigger.isUpdate && triggerContextUtility.isFirstRun()) {



    // indicates this ran
    system.debug ('***** enter leadTrigger after update *****');

    // if lead is updated by API or System User
    if(user=='API User' || user=='System User') {

        for(Lead l: trigger.new) {
          Lead oldLead = Trigger.oldMap.get(l.Id);
          Boolean oldLeadOwnedByUser  = oldLead.Owner.Type.equals('User');
          Boolean newLeadOwnedByQueue = l.Owner.Type.equals('Queue');
           if (oldLeadOwnedByUser && newLeadOwnedByQueue){  
            Date four_days_ago = Date.today().addDays(-4);
            Date seven_days_ago = Date.today().addDays(-7);
            String owneridstring = l.OwnerId;
            system.debug ('leadTrigger after update activeOrg running user: ' + user);
            system.debug ('leadTrigger after update activeOrg lead id: ' +l.Id);
            system.debug ('leadTrigger after update activeOrg org id: ' +l.Org_ID__c);
            system.debug ('leadTrigger after update activeOrg IsConverted: ' +l.IsConverted);
            system.debug ('leadTrigger after update activeOrg IsDeleted: ' +l.IsDeleted);
            system.debug ('leadTrigger after update activeOrg Scammer__c: ' +l.Scammer__c);
            system.debug ('leadTrigger after update activeOrg CustomerStatus__c: ' +l.CustomerStatus__c);
            system.debug ('leadTrigger after update activeOrg Hiring_Plan__c: ' +l.Hiring_Plan__c);
            system.debug ('leadTrigger after update activeOrg ZipHR_Demo_Request_Time__c: ' +l.ZipHR_Demo_Request_Time__c);
            system.debug ('leadTrigger after update activeOrg ZipHR_Benefits_Setup_Request_Time__c: ' +l.ZipHR_Benefits_Setup_Request_Time__c);
            system.debug ('leadTrigger after update activeOrg ZipHR_Self_Service_Benefits_Start_Time__c: ' +l.ZipHR_Self_Service_Benefits_Start_Time__c);
            system.debug ('leadTrigger after update activeOrg Free_Trial_Start_Time__c: ' +l.Free_Trial_Start_Time__c);
            system.debug ('leadTrigger after update activeOrg LastActivityDate: ' +l.LastActivityDate);
            system.debug ('leadTrigger after update activeOrg seven_days_ago: ' +seven_days_ago);
            system.debug ('leadTrigger after update activeOrg OwnerId: ' +l.OwnerId);

            // lead qualifies for auto conversion if
            // lead is a paying org 
            // lead is not on a free trial
            if(
                //trigger.oldMap.get(l.Id).Hiring_Plan__c != l.Hiring_Plan__c &&
                l.Org_ID__c != null
                && l.IsConverted == false
                && l.IsDeleted == false
                && l.Scammer__c != 'scammer'
                && l.CustomerStatus__c == 'paying'
                && l.Hiring_Plan__c != null
                && !l.Hiring_Plan__c.contains('free')
                && !l.Hiring_Plan__c.contains('trial')
                && (l.Free_Trial_Start_Time__c < four_days_ago || l.Free_Trial_Start_Time__c == null)
                /*
                && ( (l.LastActivityDate == null 
                    || l.LastActivityDate < seven_days_ago) 
                        || owneridstring.startsWith('00GG') 
                    )
                && l.ZipHR_Demo_Request_Time__c == null
                && l.ZipHR_Benefits_Setup_Request_Time__c == null
                && l.ZipHR_Self_Service_Benefits_Start_Time__c == null
                && (l.Free_Trial_Start_Time__c < seven_days_ago 
                    || l.Free_Trial_Start_Time__c == null)
                */
                ) {
                    activeOrgMap.put(l.Org_ID__c,l);
                }
            }       
        }

        system.debug ('***** leadTrigger after update activeOrgMap size: '+activeOrgMap.size());

    }

    // prevent multiple recursions
    triggerContextUtility.setNumOfRuns();
    system.debug('**** leadTrigger after update number of runs: ' + triggerContextUtility.getNumOfRuns());

    // if trigger already ran, setFirstRunFalse
    TriggerContextUtility.setFirstRunFalse();
    system.debug('**** leadTrigger after update is first run: ' + triggerContextUtility.isFirstRun());

    // return number of soql queries in this execution context so far 
    system.debug('leadTrigger after update, queries used in this apex code so far: ' + limits.getQueries());


}   /**** end after update ****/

Could something in there be the problem?

Best Answer

The first piece of code you posted will use an amount of CPU time that depends on how many records are matched and so is inherently at risk of hitting CPU and heap limits no matter how much optimisation you do.

A mechanism the platform provides to handle this problem is batchable Apex where you can place the query in the start method and have the work done in multiple transactions in the execute method where each transaction is passed a limited number of records - you can pick how many and so stay well below the governor limits. The processing is done asynchronously and for example can be scheduled to run every night (which makes sense for logic like this based on days).

PS

To start the batch processing (where the second parameter is how many records to process at once and can be from 1 to 2,000):

Database.executeBatch(new MyBatchable(), 1000);

with the batchable class:

public class ContactFieldsBatchable implements Database.Batchable<SObject> {

    public Database.QueryLocator start(Database.BatchableContext context) {
        // Can return up to 50 million records
        return Database.getQueryLocator([
                SELECT id
                FROM Lead 
                WHERE Days_Since_Last_Touch__c > 2
                AND CreatedDate = LAST_N_WEEKS:6
                AND Status = 'Attempting'
                AND Owner.Type = 'User'
                ORDER BY CreatedDate DESC NULLS LAST
                ]);
    }

    public void execute(Database.BatchableContext context, List<Lead> scope) {
        List<Group> distQueues = [
                SELECT Id
                FROM Group 
                WHERE DeveloperName = 'Distribution_Queue'
                AND Type = 'Queue'
                ORDER BY CreatedDate DESC NULLS LAST LIMIT 1
                ];
        if (distQueues.size() > 0) {
            Id distQueueId = distQueues[0].id;
            for (Lead l : scope) l.Ownerid = distQueueId;
            update scope;
        }
    }

    public void finish(Database.BatchableContext context) {
    }
}
Related Topic