Apex CPU time limit exceeded in tidy trigger pattern

I’m using Tony Scott’s Tidy Trigger pattern for triggers. Suddenly, I’m facing CPU limit exception on trying to update 2k accounts. On checking the debug log I see that validation rules & duplicate rules are being called. But I’m not sure why a working pattern has stopped working all of a sudden. The debug log registers the following –

Class.TriggerFactory.execute: line 94, column 1
Class.TriggerFactory.createAndExecuteHandler: line 27, column 1
Trigger.AccountTrigger: line 3, column 1

Here is the Account TriggerHandler, I don’t see that the any logic is written on the update events, beforeUpdate & afterUpdate are empty methods.

public with sharing class Account_TriggerHandler implements ITrigger{

    private list<Account> accList = new list<Account>();
    private Map<Id, Account> MapIdAcc = new Map<Id, Account>();
    private string TriggerEventName;


    public void bulkBefore() {

        if(Trigger.isDelete)
        {
          MapIdAcc = (Map<Id, Account>)Trigger.OldMap;
        }

        if(Trigger.isInsert){
            TriggerEventName = 'Insert';
        }
        else{
            TriggerEventName = 'Update';
        }
    }

    public void bulkAfter() {

    if(Trigger.isInsert)
        {
         accList = Trigger.new;
        }
    }

    public void beforeInsert(SObject so) {       

    }

    public void beforeUpdate(SObject oldSo, SObject so) {       


    }

    public void beforeDelete(SObject so) {    

    }

    public void afterInsert(SObject so) {

    }

    public void afterUpdate(SObject oldSo, SObject so) {

    }

    public void afterDelete(SObject so) {

    }

    public void andFinally() {
        if(Trigger.isAfter && Trigger.isInsert)
        {
        Account_TriggerHelper.sendEmailToSharedPeople(accList);
        Account_TriggerHelper.sendEmailOnBlockingCustomer(accList); 


        }

        if(Trigger.isBefore && !Trigger.isDelete){
            Account_TriggerHelper.populateApprovalGroup(accList, MapIdAcc, TriggerEventName); 
        } 
    }


}

Account_TriggerHelper –

public  class Account_TriggerHelper {
    public static void sendEmailToSharedPeople(List<Account> accList) {
        Map<Id, Account> mapOfAccIdAndAcc = new Map<Id, Account> ();
        Map<Id, Set<Id>> mapOfAccAndSharedUsers = new Map<Id, Set<Id>> ();
        Map<Id, String> mapOfUserAndName = new Map<Id, String> ();
        Set<Id> setOfGroupIds = new Set<Id> ();
        Map<Id, Set<Id>> mapOfGroupIdAndMember = new Map<Id, Set<Id>> ();
        String userType = Schema.sObjectType.User.getKeyPrefix();
        String groupType = Schema.sObjectType.Group.getKeyPrefix();

        for(sObject accS : accList) {
            Account acc = (Account)accS;
            mapOfAccIdAndAcc.put(acc.Id, acc);
        }
        for(AccountShare accShare : [SELECT Id, AccountId, UserOrGroupId
                                    FROM AccountShare
                                    WHERE AccountId IN :accList]) {
            if(!mapOfAccAndSharedUsers.containsKey(accShare.AccountId)) {
                mapOfAccAndSharedUsers.put(accShare.AccountId, new Set<Id> ());
            }
            if(String.valueOf(accShare.UserOrGroupId).startsWith(userType) && (!UserUtil.getUser(accShare.UserOrGroupId).Profile.Name.contains('Administrator'))) {
                mapOfAccAndSharedUsers.get(accShare.AccountId).add(accShare.UserOrGroupId);
            } 
            if(String.valueOf(accShare.UserOrGroupId).startsWith(groupType)) {
                setOfGroupIds.add(accShare.UserOrGroupId);
            }
        }
        for(GroupMember gm : [SELECT Id, UserOrGroupId, GroupId
                                FROM GroupMember
                                WHERE GroupId IN :setOfGroupIds]) {
            if(String.valueOf(gm.UserOrGroupId).startsWith(userType)) {
                if(!mapOfGroupIdAndMember.containsKey(gm.GroupId)) {
                    mapOfGroupIdAndMember.put(gm.GroupId, new Set<Id>());
                }
                mapOfGroupIdAndMember.get(gm.GroupId).add(gm.UserOrGroupId);
            }
        }
        for(AccountShare accShare : [SELECT Id, AccountId, UserOrGroupId
                                    FROM AccountShare
                                    WHERE UserOrGroupId IN :setOfGroupIds
                                    AND AccountId IN :mapOfAccIdAndAcc.keySet()]) {
            if(!mapOfAccAndSharedUsers.containsKey(accShare.AccountId)) {
                mapOfAccAndSharedUsers.put(accShare.AccountId, new Set<Id> ());
            }
            if(mapOfGroupIdAndMember.get(accShare.UserOrGroupId) != null) {
                mapOfAccAndSharedUsers.get(accShare.AccountId).addAll(mapOfGroupIdAndMember.get(accShare.UserOrGroupId));
            }
        }
        Set<Id> setOfUserIds = new Set<Id> ();
        for(Set<Id> lstId : mapOfAccAndSharedUsers.values()) {
            for(Id idVal : lstId) {
                if(!setOfUserIds.contains(idVal)) {
                    setOfUserIds.add(idVal);
                }
            }
        }
        for(User u : [SELECT Id, Name
                    FROM User
                    WHERE Id IN :setOfUserIds]) {
            mapOfUserAndName.put(u.Id, u.Name);
        }
        List<Messaging.SingleEmailMessage> mails = new List<Messaging.SingleEmailMessage>();
        for(Id idOfAcc : mapOfAccAndSharedUsers.keySet()) {
            for(Id idOfUser : mapOfAccAndSharedUsers.get(idOfAcc)) {
                Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
                mail.setTargetObjectId(idOfUser);
                mail.saveAsActivity = false;
                mail.setSubject('New Customer:' + mapOfAccIdAndAcc.get(idOfAcc).Name + ' has been shared with you.');
                String body = 'Dear ' + mapOfUserAndName.get(idOfUser) + ',<br/>' + '<br/>';
                body += 'Please be informed that customer ' + '<b>' + mapOfAccIdAndAcc.get(idOfAcc).Name +'</b>'+ ' has been shared with you.' + '<br/>'+ '<br/>';
                body += 'Please find the customer link ' + '<a href=" ' + MapOfAccIdAndAcc.get(idOfAcc).ORG_URL__c + String.valueOf(mapOfAccIdAndAcc.get(idOfAcc).Id) + '">here.</a>' + '<br/>' + '<br/>';
                body += 'Thanks & Regards,'+ '<br/>'+ '<br/>';
                body += 'Sales Team';
                mail.setHtmlBody(body);
                mails.add(mail);
            }
        }
        Messaging.sendEmail(mails);
    }

    public static void sendEmailOnBlockingCustomer(List<Account> accList) {
        Map<Id, String> mapOfAccIdAndName = new Map<Id, String> ();
        Map<Id, Set<Id>> mapOfAccAndSharedUsers = new Map<Id, Set<Id>> ();
        Map<Id, String> mapOfUserAndName = new Map<Id, String> ();
        Set<Id> setOfGroupIds = new Set<Id> ();
        String userType = Schema.sObjectType.User.getKeyPrefix();
        Map<Id, Set<Id>> mapOfGroupIdAndMember = new Map<Id, Set<Id>> ();
        for(sObject accV : accList) {
            Account acc = (Account)accV;
            Account oldVal = new Account();
            if(Trigger.oldMap != null && Trigger.oldMap.containsKey(acc.Id)) {
                oldVal = (Account)Trigger.oldMap.get(acc.Id);
            }
            if((Trigger.oldMap == null && (acc.Block_Customer__c == true || acc.Blocked_Customer__c == true)) || (acc.Block_Customer__c == true && oldVal.Blocked_Customer__c == false) || (acc.Blocked_Customer__c == true && oldVal.Blocked_Customer__c == false)) {
                mapOfAccIdAndName.put(acc.Id, acc.Name);
            }
        }
        for(AccountShare accShare : [SELECT Id, AccountId, UserOrGroupId
                                    FROM AccountShare
                                    WHERE AccountId IN :mapOfAccIdAndName.keySet()]) {
            if(String.valueOf(accShare.UserOrGroupId).startsWith(userType)) {
                if(!mapOfAccAndSharedUsers.containsKey(accShare.AccountId)) {
                    mapOfAccAndSharedUsers.put(accShare.AccountId, new Set<Id> ());
                }
                mapOfAccAndSharedUsers.get(accShare.AccountId).add(accShare.UserOrGroupId);
            } else {
                if(!setOfGroupIds.contains(accShare.UserOrGroupId)) {
                    setOfGroupIds.add(accShare.UserOrGroupId);
                }
            }
        }
        for(GroupMember gm : [SELECT Id, UserOrGroupId,GroupId
                                FROM GroupMember
                                WHERE GroupId IN :setOfGroupIds]) {
            if(String.valueOf(gm.UserOrGroupId).startsWith(userType)) {
                if(!mapOfGroupIdAndMember.containsKey(gm.GroupId)) {
                    mapOfGroupIdAndMember.put(gm.GroupId, new Set<Id>());
                }
                mapOfGroupIdAndMember.get(gm.GroupId).add(gm.UserOrGroupId);
            }
        }
        for(AccountShare accShare : [SELECT Id, AccountId, UserOrGroupId
                                    FROM AccountShare
                                    WHERE UserOrGroupId IN :setOfGroupIds
                                    AND AccountId IN :mapOfAccIdAndName.keySet()]) {
            if(!mapOfAccAndSharedUsers.containsKey(accShare.AccountId)) {
                mapOfAccAndSharedUsers.put(accShare.AccountId, new Set<Id> ());
            }
                                        if(mapOfGroupIdAndMember != null && mapOfGroupIdAndMember.containsKey(accShare.UserOrGroupId)) {
                                                     for(Id IdVal : mapOfGroupIdAndMember.get(accShare.UserOrGroupId)) {
                mapOfAccAndSharedUsers.get(accShare.AccountId).add(idVal);
            }   
                                        }
        }
        Set<Id> setOfUserIds = new Set<Id> ();
        for(Set<Id> lstId : mapOfAccAndSharedUsers.values()) {
            for(Id idVal : lstId) {
                if(!setOfUserIds.contains(idVal)) {
                    setOfUserIds.add(idVal);
                }
            }
        }
        for(User u : [SELECT Id, Email , Name
                        FROM User
                        WHERE Id IN :setOfUserIds]) {
            mapOfUserAndName.put(u.Id, u.Name);
        }
        List<Messaging.SingleEmailMessage> mails = new List<Messaging.SingleEmailMessage>();
        for(Id idOfAcc : mapOfAccAndSharedUsers.keySet()) {
            for(Id idOfUser : mapOfAccAndSharedUsers.get(idOfAcc)) {
                Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
                mail.setTargetObjectId(idOfUser);
                mail.saveAsActivity = false;
                mail.setSubject('Customer: ' + mapOfAccIdAndName.get(idOfAcc) + ' has been blocked.');
                String body = 'Dear ' + mapOfUserAndName.get(idOfUser) + ',<br/>' + '<br/>';
                body += 'Please be informed that customer ' + '<b>' + mapOfAccIdAndName.get(idOfAcc) +'</b>'+ ' has been blocked.' + '<br/>'+ '<br/>';
                body += 'Please find the customer link ' + '<a href=" ' + String.valueOf(mapOfAccIdAndName.get(idOfAcc)) +'">here.</a>' + '<br/>' + '<br/>';
                body += 'Thanks & Regards,';
                body += 'Sales Team';                
                mail.setHtmlBody(body);
                mails.add(mail);                             
                }
        }
        Messaging.sendEmail(mails);        
    }

    public static void populateApprovalGroup(List<Account> lstAccounts, Map<id, Account> oldMapAccounts, String strEvent){
        //Variable Declaration
        set<String> setSalesOfficeName = new Set<String>();
        Set<String> setSalesOfficeIds = new Set<String>();
        Map<String, List<Account>> mapSalesOfficeLstAccounts = new Map<String, List<Account>>();
        Map<String, List<Account>> mapSalesOfficeIdLstAccounts = new Map<String, List<Account>>();
        List<Account> lstAccountsToBeUpdated = new List<Account>();
        Id idSAPcustomer_RT = Schema.SObjectType.Account.getRecordTypeInfosByName().get('SAP Customer').getRecordTypeId();
        Boolean boolEligibleRec;

        //Iterate Over Accounts to Collect records
        for(Account iteratorAcc : lstAccounts){
            boolEligibleRec = false;
            //Block for SAP Customer
            if(iteratorAcc.recordTypeID == idSAPcustomer_RT) {
                if(strEvent == 'Insert') {
                    boolEligibleRec = true;
                }
                else if(strEvent == 'Update') {
                    if(oldMapAccounts.get(iteratorAcc.id).Sales_office_via_interface__c != iteratorAcc.Sales_office_via_interface__c) {
                       boolEligibleRec = true;
                    }
                }
                if(boolEligibleRec) {
                    setSalesOfficeIds.add(iteratorAcc.Sales_office_via_interface__c);
                    if(mapSalesOfficeIdLstAccounts.containsKey(iteratorAcc.Sales_office_via_interface__c)) {
                        mapSalesOfficeIdLstAccounts.get(iteratorAcc.Sales_office_via_interface__c).add(iteratorAcc);
                    } else {
                        mapSalesOfficeIdLstAccounts.put(iteratorAcc.Sales_office_via_interface__c, new List<Account>{iteratorAcc});
                    }
                }
            }
        }
        //Modify Block for SalesOffice Ids
        if(setSalesOfficeIds.size()>0) {
            List<Sales_Office__c> lstSalesOffice = [select Id, Name from Sales_Office__c WHERE Id =: setSalesOfficeIds];
            for(Sales_Office__c iteratorSalesOffice: lstSalesOffice) {
                setSalesOfficeName.add(iteratorSalesOffice.Name);
                if(mapSalesOfficeLstAccounts.containsKey(iteratorSalesOffice.Name)) {
                    mapSalesOfficeLstAccounts.get(iteratorSalesOffice.Name).addAll(mapSalesOfficeIdLstAccounts.get(iteratorSalesOffice.id));
                } else {
                    mapSalesOfficeLstAccounts.put(iteratorSalesOffice.Name, mapSalesOfficeIdLstAccounts.get(iteratorSalesOffice.id));
                }
            }
        }
        List<Sales_Office_and_Group_Mapping__c> lstSalesAndGroupMapping = [select Name, Group__c from Sales_Office_and_Group_Mapping__c WHERE Name=: setSalesOfficeName];
        for(Sales_Office_and_Group_Mapping__c iteratorSalesAndGroupMapping: lstSalesAndGroupMapping) { 
            for(Account varIteratorAcc : mapSalesOfficeLstAccounts.get(iteratorSalesAndGroupMapping.Name)) {
                varIteratorAcc.Approval_Group__c = iteratorSalesAndGroupMapping.Group__c;
                lstAccountsToBeUpdated.add(varIteratorAcc);
            }
        }
    }
}

This is the trigger –

trigger AccountTrigger on Account (after delete, after insert, after update, before delete, before insert, before update)
    {
    TriggerFactory.createAndExecuteHandler(Account_TriggerHandler.class);

    }

& following is the TriggerFactory –

public with sharing class TriggerFactory
{
    /**
     * Public static method to create and execute a trigger handler
     *
     * Arguments:   Type t - Type of handler to instatiate
     *
     * Throws a TriggerException if no handler has been found.
     */
    public static void createAndExecuteHandler(Type t)
    {
        // Get a handler appropriate to the object being processed
        ITrigger handler = getHandler(t);

        // Make sure we have a handler registered, new handlers must be registered in the getHandler method.
        if (handler == null)
        {
            throw new TriggerException('No Trigger Handler found named: ' + t.getName());
        }

        // Execute the handler to fulfil the trigger
        execute(handler);
    }

    /**
     * private static method to control the execution of the handler
     *
     * Arguments:   ITrigger handler - A Trigger Handler to execute
     */
    private static void execute(ITrigger handler)
    {
        // Before Trigger
        if (Trigger.isBefore)
        {
            // Call the bulk before to handle any caching of data and enable bulkification
            handler.bulkBefore();

            // Iterate through the records to be deleted passing them to the handler.
            if (Trigger.isDelete)
            {
                for (SObject so : Trigger.old)
                {
                    handler.beforeDelete(so);
                }
            }
            // Iterate through the records to be inserted passing them to the handler.
            else if (Trigger.isInsert)
            {
                for (SObject so : Trigger.new)
                {
                    handler.beforeInsert(so);
                }
            }
            // Iterate through the records to be updated passing them to the handler.
            else if (Trigger.isUpdate)
            {
                for (SObject so : Trigger.old)
                {
                    handler.beforeUpdate(so, Trigger.newMap.get(so.Id));
                }
            }
        }
        else
        {
            // Call the bulk after to handle any caching of data and enable bulkification
            handler.bulkAfter();

            // Iterate through the records deleted passing them to the handler.
            if (Trigger.isDelete)
            {
                for (SObject so : Trigger.old)
                {
                    handler.afterDelete(so);
                }
            }
            // Iterate through the records inserted passing them to the handler.
            else if (Trigger.isInsert)
            {
                for (SObject so : Trigger.new)
                {
                    handler.afterInsert(so);
                }
            }
            // Iterate through the records updated passing them to the handler.
            else if (Trigger.isUpdate)
            {
              for (SObject so : Trigger.old)
                  {
                     handler.afterUpdate(so, Trigger.newMap.get(so.Id));
                  }
            }      
        }

        // Perform any post processing
        handler.andFinally();
    }

    /**
     * private static method to get the named handler.
     *
     * Arguments:   Type t - Class of handler to instatiate
     *
     * Returns:     ITrigger - A trigger handler if one exists or null.
     */
    private static ITrigger getHandler(Type t)
    {
        // Instantiate the type
        Object o = t.newInstance();

        // if its not an instance of ITrigger return null
        if (!(o instanceOf ITrigger))
        {
            return null;
        }

        return (ITrigger)o;
    }

    public class TriggerException extends Exception {}
}

Line 94 on the TriggerFactory is –

 handler.afterUpdate(so, Trigger.newMap.get(so.Id));

Line 27 on the TriggerFactory is –

execute(handler);

Line 3 on the Trigger is –

TriggerFactory.createAndExecuteHandler(Account_TriggerHandler.class);

The debug log reveals too less about the classes & I see validation & duplicate rules being loggedLink to debug log

Answer

I did a visualization of your debug log. It looks something like this:

Debug Log Visualization

Yes, it does look like some incomprehensible colored lines, but it is showing the repetition from the blocks of 200 records being processed through before update triggers (red), Validation (green), after update triggers (orange), workflow (purple).

If you look at it as a tree you can see where the time from each iteration is going.

enter image description here

Your before and after update triggers are contributing to the CPU usage, but the Workflow is using much more per iteration. In the tree above, you can see 31 milliseconds for the first before update AccountTrigger, 11 ms for the AfterUpdate AccountTrigger. Compare that to the 134 ms for the Account Workflow.

DuplicateDetector is also taking 40 ms per iteration.

Also, don’t discount the validation steps even though each one only takes a small amount of time. 200 times that small amount of time can add up.

“Time spent evaluating formulas for validation rules or workflows are counted towards the limit.” Source

The log you provided doesn’t have the Workflow logging, but that is where I’d focus my attention. Often the thing that triggers a CPU limit is just the unfortunate bit of code that happened to be running at the time. See Dan Appleman’s The Dark Art Of CPU Benchmarking from Dreamforce 2016.

Attribution
Source : Link , Question Author : Jarvis , Answer Author : Daniel Ballinger

Leave a Comment